├── .bingo ├── .gitignore ├── README.md ├── Variables.mk ├── calens.mod ├── calens.sum ├── go.mod ├── reflex.mod ├── reflex.sum ├── revive.mod ├── revive.sum ├── staticcheck.mod ├── staticcheck.sum └── variables.env ├── .codacy.yml ├── .dockerignore ├── .editorconfig ├── .github ├── issue_template.md ├── pull_request_template.md ├── renovate.json ├── settings.yml └── workflows │ ├── automerge.yml │ ├── binaries.yml │ ├── changes.yml │ ├── docker.yml │ ├── docs.yml │ ├── flake.yml │ └── general.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DCO ├── LICENSE ├── Makefile ├── README.md ├── changelog ├── 1.0.0_2022-05-05 │ └── initial-release.md ├── 1.0.1_2022-05-10 │ └── fix-bindings.md ├── CHANGELOG.tmpl ├── README.md ├── TEMPLATE └── unreleased │ ├── .keep │ └── config-file.md ├── cmd └── terrastate │ └── main.go ├── config ├── example.json └── example.yaml ├── docker ├── Dockerfile.linux.386 ├── Dockerfile.linux.amd64 ├── Dockerfile.linux.arm └── Dockerfile.linux.arm64 ├── docs ├── .gitignore ├── archetypes │ └── default.md ├── config.toml ├── content │ ├── about.md │ ├── building.md │ ├── getting-started.md │ └── license.md ├── layouts │ ├── index.html │ ├── partials │ │ └── style.html │ └── shortcodes │ │ └── partial.html ├── partials │ └── envvars.md └── static │ └── syntax.css ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── pkg ├── command │ ├── cmd.go │ ├── cmd_test.go │ ├── health.go │ ├── server.go │ ├── setup.go │ └── state.go ├── config │ └── config.go ├── handler │ ├── delete.go │ ├── fetch.go │ ├── handler.go │ ├── lock.go │ ├── metrics.go │ ├── unlock.go │ └── update.go ├── helper │ └── encryption.go ├── middleware │ ├── basicauth │ │ └── basicauth.go │ ├── header │ │ └── header.go │ └── prometheus │ │ └── prometheus.go ├── model │ └── lock_info.go ├── router │ └── router.go └── version │ ├── collector.go │ └── version.go ├── reflex.conf └── revive.toml /.bingo/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Ignore everything 3 | * 4 | 5 | # But not these files: 6 | !.gitignore 7 | !*.mod 8 | !*.sum 9 | !README.md 10 | !Variables.mk 11 | !variables.env 12 | 13 | *tmp.mod 14 | -------------------------------------------------------------------------------- /.bingo/README.md: -------------------------------------------------------------------------------- 1 | # Project Development Dependencies. 2 | 3 | This is directory which stores Go modules with pinned buildable package that is used within this repository, managed by https://github.com/bwplotka/bingo. 4 | 5 | * Run `bingo get` to install all tools having each own module file in this directory. 6 | * Run `bingo get ` to install that have own module file in this directory. 7 | * For Makefile: Make sure to put `include .bingo/Variables.mk` in your Makefile, then use $() variable where is the .bingo/.mod. 8 | * For shell: Run `source .bingo/variables.env` to source all environment variable for each tool. 9 | * For go: Import `.bingo/variables.go` to for variable names. 10 | * See https://github.com/bwplotka/bingo or -h on how to add, remove or change binaries dependencies. 11 | 12 | ## Requirements 13 | 14 | * Go 1.14+ 15 | -------------------------------------------------------------------------------- /.bingo/Variables.mk: -------------------------------------------------------------------------------- 1 | # Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.9. DO NOT EDIT. 2 | # All tools are designed to be build inside $GOBIN. 3 | BINGO_DIR := $(dir $(lastword $(MAKEFILE_LIST))) 4 | GOPATH ?= $(shell go env GOPATH) 5 | GOBIN ?= $(firstword $(subst :, ,${GOPATH}))/bin 6 | GO ?= $(shell which go) 7 | 8 | # Below generated variables ensure that every time a tool under each variable is invoked, the correct version 9 | # will be used; reinstalling only if needed. 10 | # For example for calens variable: 11 | # 12 | # In your main Makefile (for non array binaries): 13 | # 14 | #include .bingo/Variables.mk # Assuming -dir was set to .bingo . 15 | # 16 | #command: $(CALENS) 17 | # @echo "Running calens" 18 | # @$(CALENS) 19 | # 20 | CALENS := $(GOBIN)/calens-v0.4.0 21 | $(CALENS): $(BINGO_DIR)/calens.mod 22 | @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. 23 | @echo "(re)installing $(GOBIN)/calens-v0.4.0" 24 | @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=calens.mod -o=$(GOBIN)/calens-v0.4.0 "github.com/restic/calens" 25 | 26 | REFLEX := $(GOBIN)/reflex-v0.3.1 27 | $(REFLEX): $(BINGO_DIR)/reflex.mod 28 | @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. 29 | @echo "(re)installing $(GOBIN)/reflex-v0.3.1" 30 | @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=reflex.mod -o=$(GOBIN)/reflex-v0.3.1 "github.com/cespare/reflex" 31 | 32 | REVIVE := $(GOBIN)/revive-v1.3.9 33 | $(REVIVE): $(BINGO_DIR)/revive.mod 34 | @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. 35 | @echo "(re)installing $(GOBIN)/revive-v1.3.9" 36 | @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=revive.mod -o=$(GOBIN)/revive-v1.3.9 "github.com/mgechev/revive" 37 | 38 | STATICCHECK := $(GOBIN)/staticcheck-v0.5.1 39 | $(STATICCHECK): $(BINGO_DIR)/staticcheck.mod 40 | @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. 41 | @echo "(re)installing $(GOBIN)/staticcheck-v0.5.1" 42 | @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=staticcheck.mod -o=$(GOBIN)/staticcheck-v0.5.1 "honnef.co/go/tools/cmd/staticcheck" 43 | 44 | -------------------------------------------------------------------------------- /.bingo/calens.mod: -------------------------------------------------------------------------------- 1 | module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT 2 | 3 | go 1.17 4 | 5 | require github.com/restic/calens v0.4.0 6 | -------------------------------------------------------------------------------- /.bingo/calens.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= 2 | github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 3 | github.com/Masterminds/semver/v3 v3.0.2 h1:tRi7ENs+AaOUCH+j6qwNQgPYfV26dX3JNonq+V4mhqc= 4 | github.com/Masterminds/semver/v3 v3.0.2/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 5 | github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= 6 | github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 7 | github.com/Masterminds/sprig/v3 v3.0.1 h1:RuaOafp+8qOLUPX1lInLfUrLc1MEVbnz7a40RLoixKY= 8 | github.com/Masterminds/sprig/v3 v3.0.1/go.mod h1:Cp7HwZjmqKrC+Y7XqSJOU2yRvAJRGLiohfgz5ZJj8+4= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 12 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 13 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 14 | github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= 15 | github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= 16 | github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= 17 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 18 | github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= 19 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 20 | github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= 21 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/restic/calens v0.2.0 h1:LVNAtmFc+Pb4ODX66qdX1T3Di1P0OTLyUsVyvM/xD7E= 24 | github.com/restic/calens v0.2.0/go.mod h1:UXwyAKS4wsgUZGEc7NrzzygJbLsQZIo3wl+62Q1wvmU= 25 | github.com/restic/calens v0.3.0 h1:GtkB4butZZQ+GyKYlIGN3SVKvOdaR8eZ/81wkitaUFI= 26 | github.com/restic/calens v0.3.0/go.mod h1:UXwyAKS4wsgUZGEc7NrzzygJbLsQZIo3wl+62Q1wvmU= 27 | github.com/restic/calens v0.4.0 h1:j0K2Lv1cnvD3Q2v/ULitLHqAOFKoCH2djBCmJ/Gzqvk= 28 | github.com/restic/calens v0.4.0/go.mod h1:ZBRZWv467s1l+a4O92N9vu7zQ37sHvBhfHRPOdPT4rg= 29 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 30 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 31 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 32 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 33 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 34 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 35 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 36 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 37 | golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU= 38 | golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 39 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 40 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 41 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 45 | -------------------------------------------------------------------------------- /.bingo/go.mod: -------------------------------------------------------------------------------- 1 | module _ // Fake go.mod auto-created by 'bingo' for go -moddir compatibility with non-Go projects. Commit this file, together with other .mod files. -------------------------------------------------------------------------------- /.bingo/reflex.mod: -------------------------------------------------------------------------------- 1 | module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT 2 | 3 | go 1.17 4 | 5 | require github.com/cespare/reflex v0.3.1 6 | -------------------------------------------------------------------------------- /.bingo/reflex.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/reflex v0.3.1 h1:N4Y/UmRrjwOkNT0oQQnYsdr6YBxvHqtSfPB4mqOyAKk= 2 | github.com/cespare/reflex v0.3.1/go.mod h1:I+0Pnu2W693i7Hv6ZZG76qHTY0mgUa7uCIfCtikXojE= 3 | github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= 4 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 6 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 7 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 8 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 9 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 10 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 11 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 12 | github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= 13 | github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= 14 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= 15 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 16 | -------------------------------------------------------------------------------- /.bingo/revive.mod: -------------------------------------------------------------------------------- 1 | module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT 2 | 3 | go 1.21 4 | 5 | toolchain go1.23.0 6 | 7 | require github.com/mgechev/revive v1.3.9 8 | -------------------------------------------------------------------------------- /.bingo/revive.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 4 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 5 | github.com/chavacava/garif v0.0.0-20221024190013-b3ef35877348 h1:cy5GCEZLUCshCGCRRUjxHrDUqkB4l5cuUt3ShEckQEo= 6 | github.com/chavacava/garif v0.0.0-20221024190013-b3ef35877348/go.mod h1:f/miWtG3SSuTxKsNK3o58H1xl+XV6ZIfbC6p7lPPB8U= 7 | github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= 8 | github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= 12 | github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= 13 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 14 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 15 | github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= 16 | github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= 17 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 18 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 19 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 20 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 21 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 22 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 23 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 24 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 25 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 26 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 27 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 28 | github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 h1:zpIH83+oKzcpryru8ceC6BxnoG8TBrhgAvRg8obzup0= 29 | github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= 30 | github.com/mgechev/revive v1.2.5 h1:UF9AR8pOAuwNmhXj2odp4mxv9Nx2qUIwVz8ZsU+Mbec= 31 | github.com/mgechev/revive v1.2.5/go.mod h1:nFOXent79jMTISAfOAasKfy0Z2Ejq0WX7Qn/KAdYopI= 32 | github.com/mgechev/revive v1.3.9 h1:18Y3R4a2USSBF+QZKFQwVkBROUda7uoBlkEuBD+YD1A= 33 | github.com/mgechev/revive v1.3.9/go.mod h1:+uxEIr5UH0TjXWHTno3xh4u7eg6jDpXKzQccA9UGhHU= 34 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 35 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 36 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 37 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 38 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 39 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 42 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 43 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 44 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 45 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 46 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 47 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 48 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 49 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 50 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 51 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 52 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 55 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 56 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 57 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 58 | golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= 59 | golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= 60 | golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= 61 | golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 63 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 64 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 65 | -------------------------------------------------------------------------------- /.bingo/staticcheck.mod: -------------------------------------------------------------------------------- 1 | module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT 2 | 3 | go 1.22.1 4 | 5 | toolchain go1.22.3 6 | 7 | require honnef.co/go/tools v0.5.1 // cmd/staticcheck 8 | -------------------------------------------------------------------------------- /.bingo/staticcheck.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= 2 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 3 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 4 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= 5 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 6 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 7 | golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= 8 | golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE= 9 | golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 10 | golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= 11 | golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 12 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= 13 | golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= 14 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 15 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 16 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 17 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 18 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 19 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= 20 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 21 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/tools v0.1.11-0.20220316014157-77aa08bb151a h1:ofrrl6c6NG5/IOSx/R1cyiQxxjqlur0h/TvbUhkH0II= 23 | golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d h1:9ZNWAi4CYhNv60mXGgAncgq7SGc5qa7C8VZV8Tg7Ggs= 24 | golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 25 | golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3 h1:SHq4Rl+B7WvyM4XODon1LXtP7gcG49+7Jubt1gWWswY= 26 | golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3/go.mod h1:bqv7PJ/TtlrzgJKhOAGdDUkUltQapRik/UEHubLVBWo= 27 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 28 | honnef.co/go/tools v0.3.1 h1:1kJlrWJLkaGXgcaeosRXViwviqjI7nkBvU2+sZW0AYc= 29 | honnef.co/go/tools v0.3.1/go.mod h1:vlRD9XErLMGT+mDuofSr0mMMquscM/1nQqtRSsh6m70= 30 | honnef.co/go/tools v0.4.2 h1:6qXr+R5w+ktL5UkwEbPp+fEvfyoMPche6GkOpGHZcLc= 31 | honnef.co/go/tools v0.4.2/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA= 32 | honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= 33 | honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= 34 | -------------------------------------------------------------------------------- /.bingo/variables.env: -------------------------------------------------------------------------------- 1 | # Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.9. DO NOT EDIT. 2 | # All tools are designed to be build inside $GOBIN. 3 | # Those variables will work only until 'bingo get' was invoked, or if tools were installed via Makefile's Variables.mk. 4 | GOBIN=${GOBIN:=$(go env GOBIN)} 5 | 6 | if [ -z "$GOBIN" ]; then 7 | GOBIN="$(go env GOPATH)/bin" 8 | fi 9 | 10 | 11 | CALENS="${GOBIN}/calens-v0.4.0" 12 | 13 | REFLEX="${GOBIN}/reflex-v0.3.1" 14 | 15 | REVIVE="${GOBIN}/revive-v1.3.9" 16 | 17 | STATICCHECK="${GOBIN}/staticcheck-v0.5.1" 18 | 19 | -------------------------------------------------------------------------------- /.codacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - .github/** 4 | - .bingo/** 5 | - changelog/** 6 | - docs/** 7 | 8 | - CHANGELOG.md 9 | 10 | ... 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !bin/ 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [Makefile] 11 | indent_style = tab 12 | indent_size = 4 13 | 14 | [*.go] 15 | indent_style = tab 16 | indent_size = 4 17 | 18 | [*.md] 19 | trim_trailing_whitespace = true 20 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>webhippie/.github//renovate/preset" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | repository: 3 | name: terrastate 4 | description: Terraform HTTP remote state storage 5 | homepage: https://webhippie.github.io/terrastate/ 6 | topics: terraform, remote, state, storage 7 | 8 | private: false 9 | has_issues: true 10 | has_wiki: false 11 | has_downloads: false 12 | 13 | default_branch: master 14 | 15 | allow_squash_merge: true 16 | allow_merge_commit: true 17 | allow_rebase_merge: true 18 | 19 | allow_update_branch: true 20 | allow_auto_merge: true 21 | delete_branch_on_merge: true 22 | enable_automated_security_fixes: true 23 | enable_vulnerability_alerts: true 24 | 25 | branches: 26 | - name: master 27 | protection: 28 | required_pull_request_reviews: null 29 | required_status_checks: 30 | strict: true 31 | contexts: 32 | - testing 33 | enforce_admins: false 34 | restrictions: 35 | apps: 36 | - webhippie 37 | - renovate 38 | users: [] 39 | teams: 40 | - admins 41 | - bots 42 | - members 43 | 44 | teams: 45 | - name: admins 46 | permission: admin 47 | - name: bots 48 | permission: admin 49 | - name: members 50 | permission: maintain 51 | 52 | labels: 53 | - name: bug 54 | color: fc2929 55 | description: Something isn't working 56 | - name: duplicate 57 | color: cccccc 58 | description: This issue or pull request already exists 59 | - name: enhancement 60 | color: 84b6eb 61 | description: New feature or request 62 | - name: good first issue 63 | color: 7057ff 64 | description: Good for newcomers 65 | - name: help wanted 66 | color: 159818 67 | description: Extra attention is needed 68 | - name: invalid 69 | color: e6e6e6 70 | description: This doesn't seem right 71 | - name: question 72 | color: cc317c 73 | description: Further information is requested 74 | - name: renovate 75 | color: 1d76db 76 | description: Automated action from Renovate 77 | - name: wontfix 78 | color: 5319e7 79 | description: This will not be worked on 80 | - name: hacktoberfest 81 | color: d4c5f9 82 | description: Contribution at Hacktoberfest appreciated 83 | - name: ready 84 | color: ededed 85 | description: This is ready to be worked on 86 | - name: in progress 87 | color: ededed 88 | description: This is currently worked on 89 | - name: infra 90 | color: 006b75 91 | description: Related to the infrastructure 92 | - name: lint 93 | color: fbca04 94 | description: Related to linting tools 95 | - name: poc 96 | color: c2e0c6 97 | description: Proof of concept for new feature 98 | - name: rebase 99 | color: ffa8a5 100 | description: Branch requires a rebase 101 | - name: third-party 102 | color: e99695 103 | description: Depends on third-party tool or library 104 | - name: translation 105 | color: b60205 106 | description: Change or issue related to translations 107 | - name: ci 108 | color: b60105 109 | description: Related to Continous Integration 110 | - name: docs 111 | color: b60305 112 | description: Related to documentation 113 | - name: outdated 114 | color: cccccc 115 | description: This is out of scope and outdated 116 | 117 | ... 118 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: automerge 3 | 4 | "on": 5 | workflow_dispatch: 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | jobs: 15 | dependabot: 16 | runs-on: ubuntu-latest 17 | if: github.actor == 'dependabot[bot]' 18 | 19 | steps: 20 | - name: Generate token 21 | id: token 22 | uses: tibdex/github-app-token@v2 23 | with: 24 | app_id: ${{ secrets.TOKEN_EXCHANGE_APP }} 25 | installation_retrieval_mode: id 26 | installation_retrieval_payload: ${{ secrets.TOKEN_EXCHANGE_INSTALL }} 27 | private_key: ${{ secrets.TOKEN_EXCHANGE_KEY }} 28 | permissions: >- 29 | {"contents": "write", "pull_requests": "write", "issues": "write"} 30 | 31 | - name: Fetch metadata 32 | id: metadata 33 | uses: dependabot/fetch-metadata@v2 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Approve request 38 | id: approve 39 | run: gh pr review --approve "${{github.event.pull_request.html_url}}" 40 | env: 41 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | 43 | - name: Enable automerge 44 | id: automerge 45 | run: gh pr merge --rebase --auto "${{github.event.pull_request.html_url}}" 46 | env: 47 | GH_TOKEN: ${{ steps.token.outputs.token }} 48 | 49 | ... 50 | -------------------------------------------------------------------------------- /.github/workflows/binaries.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: binaries 3 | 4 | "on": 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - v* 10 | 11 | jobs: 12 | binaries: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout source 17 | id: source 18 | uses: actions/checkout@v4 19 | 20 | - name: Configure aws 21 | id: aws 22 | uses: aws-actions/configure-aws-credentials@v4 23 | with: 24 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 25 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 26 | aws-region: eu-central-1 27 | 28 | - name: Setup golang 29 | id: golang 30 | uses: actions/setup-go@v5 31 | with: 32 | go-version: ^1.23.0 33 | 34 | - name: Run generate 35 | id: generate 36 | run: make generate 37 | 38 | - name: Run release 39 | id: release 40 | run: make release 41 | 42 | - name: Sign release 43 | id: gpgsign 44 | uses: actionhippie/gpgsign@v1 45 | with: 46 | private_key: ${{ secrets.GNUPG_KEY }} 47 | passphrase: ${{ secrets.GNUPG_PASSWORD }} 48 | detach_sign: true 49 | files: dist/* 50 | excludes: dist/*.sha256 51 | 52 | - name: Build changes 53 | id: changelog 54 | if: startsWith(github.ref, 'refs/tags/') 55 | uses: actionhippie/calens@v1 56 | with: 57 | version: ${{ github.ref }} 58 | 59 | - name: Upload release 60 | id: upload 61 | if: startsWith(github.ref, 'refs/tags/') 62 | uses: ncipollo/release-action@v1 63 | with: 64 | token: ${{ secrets.GITHUB_TOKEN }} 65 | body: ${{ steps.changelog.outputs.generated }} 66 | artifacts: dist/* 67 | 68 | - name: Upload version 69 | id: version 70 | if: startsWith(github.ref, 'refs/tags/') 71 | run: | 72 | aws s3 sync dist/ s3://dl.webhippie.de/terrastate/${{ github.ref_name }}/ 73 | 74 | - name: Upload testing 75 | id: testing 76 | if: startsWith(github.ref, 'refs/heads/') 77 | run: | 78 | aws s3 sync dist/ s3://dl.webhippie.de/terrastate/testing/ 79 | 80 | ... 81 | -------------------------------------------------------------------------------- /.github/workflows/changes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: changes 3 | 4 | "on": 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | changelog: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout source 15 | id: source 16 | uses: actions/checkout@v4 17 | with: 18 | token: ${{ secrets.BOT_ACCESS_TOKEN }} 19 | 20 | - name: Setup golang 21 | id: golang 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ^1.23.0 25 | 26 | - name: Run changelog 27 | id: changelog 28 | run: make changelog 29 | 30 | - name: Commit changes 31 | id: commit 32 | uses: EndBug/add-and-commit@v9 33 | with: 34 | author_name: GitHub Actions 35 | author_email: github@webhippie.de 36 | add: CHANGELOG.md 37 | message: "docs: automated changelog update" 38 | push: true 39 | commit: --signoff 40 | 41 | ... 42 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: docker 3 | 4 | "on": 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - v* 10 | 11 | jobs: 12 | docker: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | include: 18 | - platform: linux/386 19 | goos: linux 20 | goarch: 386 21 | - platform: linux/amd64 22 | goos: linux 23 | goarch: amd64 24 | - platform: linux/arm64 25 | goos: linux 26 | goarch: arm64 27 | - platform: linux/arm/6 28 | goos: linux 29 | goarch: arm 30 | goarm: 6 31 | 32 | steps: 33 | - name: Checkout source 34 | id: source 35 | uses: actions/checkout@v4 36 | 37 | - name: Setup golang 38 | id: golang 39 | uses: actions/setup-go@v5 40 | with: 41 | go-version: ^1.23.0 42 | 43 | - name: Run generate 44 | id: generate 45 | env: 46 | GOOS: ${{ matrix.goos }} 47 | GOARCH: ${{ matrix.goarch }} 48 | GOARM: ${{ matrix.goarm }} 49 | run: make generate 50 | 51 | - name: Run build 52 | id: build 53 | env: 54 | GOOS: ${{ matrix.goos }} 55 | GOARCH: ${{ matrix.goarch }} 56 | GOARM: ${{ matrix.goarm }} 57 | run: make build 58 | 59 | - name: Docker meta 60 | id: meta 61 | uses: docker/metadata-action@v5 62 | with: 63 | github-token: ${{ secrets.GITHUB_TOKEN }} 64 | images: | 65 | webhippie/terrastate 66 | quay.io/webhippie/terrastate 67 | ghcr.io/webhippie/terrastate 68 | labels: | 69 | org.opencontainers.image.vendor=Webhippie 70 | maintainer=Thomas Boerger 71 | tags: | 72 | type=ref,event=pr 73 | type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} 74 | type=semver,pattern={{version}} 75 | type=semver,pattern={{major}}.{{minor}} 76 | type=semver,pattern={{major}} 77 | flavor: | 78 | suffix=-${{ matrix.goos }}-${{ matrix.goarch }} 79 | 80 | - name: Setup qemu 81 | id: qemu 82 | uses: docker/setup-qemu-action@v3 83 | 84 | - name: Setup buildx 85 | id: buildx 86 | uses: docker/setup-buildx-action@v3 87 | 88 | - name: Hub login 89 | id: login1 90 | uses: docker/login-action@v3 91 | if: github.event_name != 'pull_request' 92 | with: 93 | username: ${{ secrets.DOCKER_USERNAME }} 94 | password: ${{ secrets.DOCKER_PASSWORD }} 95 | 96 | - name: Quay login 97 | id: login2 98 | uses: docker/login-action@v3 99 | if: github.event_name != 'pull_request' 100 | with: 101 | registry: quay.io 102 | username: ${{ secrets.QUAY_USERNAME }} 103 | password: ${{ secrets.QUAY_PASSWORD }} 104 | 105 | - name: Ghcr login 106 | id: login3 107 | uses: docker/login-action@v3 108 | if: github.event_name != 'pull_request' 109 | with: 110 | registry: ghcr.io 111 | username: ${{ github.actor }} 112 | password: ${{ secrets.GITHUB_TOKEN }} 113 | 114 | - name: Build image 115 | id: publish 116 | uses: docker/build-push-action@v6 117 | with: 118 | builder: ${{ steps.buildx.outputs.name }} 119 | context: . 120 | provenance: false 121 | file: docker/Dockerfile.${{ matrix.goos }}.${{ matrix.goarch }} 122 | platforms: ${{ matrix.platform }} 123 | push: ${{ github.event_name != 'pull_request' }} 124 | labels: ${{ steps.meta.outputs.labels }} 125 | tags: ${{ steps.meta.outputs.tags }} 126 | 127 | manifest: 128 | runs-on: ubuntu-latest 129 | needs: docker 130 | 131 | steps: 132 | - name: Checkout source 133 | id: source 134 | uses: actions/checkout@v4 135 | 136 | - name: Hub tags 137 | id: hubTags 138 | uses: docker/metadata-action@v5 139 | with: 140 | github-token: ${{ secrets.GITHUB_TOKEN }} 141 | images: webhippie/terrastate 142 | tags: | 143 | type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} 144 | type=semver,pattern={{version}} 145 | type=semver,pattern={{major}}.{{minor}} 146 | type=semver,pattern={{major}} 147 | 148 | - name: Hub manifest 149 | id: hub 150 | uses: actionhippie/manifest@v1 151 | with: 152 | username: ${{ secrets.DOCKER_USERNAME }} 153 | password: ${{ secrets.DOCKER_PASSWORD }} 154 | platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6 155 | template: webhippie/terrastate:VERSION-OS-ARCH 156 | target: ${{ steps.hubTags.outputs.tags }} 157 | ignore_missing: true 158 | 159 | - name: Quay tags 160 | id: quayTags 161 | uses: docker/metadata-action@v5 162 | with: 163 | github-token: ${{ secrets.GITHUB_TOKEN }} 164 | images: quay.io/webhippie/terrastate 165 | tags: | 166 | type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} 167 | type=semver,pattern={{version}} 168 | type=semver,pattern={{major}}.{{minor}} 169 | type=semver,pattern={{major}} 170 | 171 | - name: Quay manifest 172 | id: quay 173 | uses: actionhippie/manifest@v1 174 | with: 175 | username: ${{ secrets.QUAY_USERNAME }} 176 | password: ${{ secrets.QUAY_PASSWORD }} 177 | platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6 178 | template: quay.io/webhippie/terrastate:VERSION-OS-ARCH 179 | target: ${{ steps.quayTags.outputs.tags }} 180 | ignore_missing: true 181 | 182 | - name: Ghcr tags 183 | id: ghcrTags 184 | uses: docker/metadata-action@v5 185 | with: 186 | github-token: ${{ secrets.GITHUB_TOKEN }} 187 | images: ghcr.io/webhippie/terrastate 188 | tags: | 189 | type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} 190 | type=semver,pattern={{version}} 191 | type=semver,pattern={{major}}.{{minor}} 192 | type=semver,pattern={{major}} 193 | 194 | - name: Ghcr manifest 195 | id: ghcr 196 | uses: actionhippie/manifest@v1 197 | with: 198 | username: ${{ github.actor }} 199 | password: ${{ secrets.GITHUB_TOKEN }} 200 | platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6 201 | template: ghcr.io/webhippie/terrastate:VERSION-OS-ARCH 202 | target: ${{ steps.ghcrTags.outputs.tags }} 203 | ignore_missing: true 204 | 205 | readme: 206 | runs-on: ubuntu-latest 207 | needs: docker 208 | 209 | steps: 210 | - name: Checkout source 211 | id: source 212 | uses: actions/checkout@v4 213 | 214 | - name: Hub readme 215 | id: hub 216 | uses: actionhippie/pushrm@v1 217 | with: 218 | provider: dockerhub 219 | target: webhippie/terrastate 220 | username: ${{ secrets.DOCKER_USERNAME }} 221 | password: ${{ secrets.DOCKER_PASSWORD }} 222 | description: Terrastate 223 | readme: README.md 224 | 225 | - name: Quay readme 226 | id: quay 227 | uses: actionhippie/pushrm@v1 228 | with: 229 | provider: quay 230 | target: quay.io/webhippie/terrastate 231 | apikey: ${{ secrets.QUAY_APIKEY }} 232 | readme: README.md 233 | 234 | ... 235 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: docs 3 | 4 | "on": 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | docs: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout source 15 | id: source 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup hugo 19 | id: hugo 20 | uses: peaceiris/actions-hugo@v3 21 | with: 22 | hugo-version: latest 23 | extended: true 24 | 25 | - name: Run docs 26 | id: docs 27 | run: make docs 28 | 29 | - name: Deploy pages 30 | id: deploy 31 | uses: peaceiris/actions-gh-pages@v4 32 | with: 33 | github_token: ${{ secrets.GITHUB_TOKEN }} 34 | publish_dir: docs/public/ 35 | 36 | ... 37 | -------------------------------------------------------------------------------- /.github/workflows/flake.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: flake 3 | 4 | "on": 5 | workflow_dispatch: 6 | schedule: 7 | - cron: "0 8 * * 1" 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | flake: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout source 18 | id: source 19 | uses: actions/checkout@v4 20 | with: 21 | token: ${{ secrets.BOT_ACCESS_TOKEN }} 22 | 23 | - name: Install nix 24 | id: nix 25 | uses: cachix/install-nix-action@v31 26 | 27 | - name: Update flake 28 | id: flake 29 | run: nix flake update 30 | 31 | - name: Source rebase 32 | id: rebase 33 | run: git pull --autostash --rebase 34 | 35 | - name: Commit changes 36 | uses: EndBug/add-and-commit@v9 37 | with: 38 | author_name: GitHub Actions 39 | author_email: github@webhippie.de 40 | add: flake.lock 41 | message: "chore(flake): updated lockfile [skip ci]" 42 | push: true 43 | commit: --signoff 44 | 45 | ... 46 | -------------------------------------------------------------------------------- /.github/workflows/general.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: general 3 | 4 | "on": 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | testing: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout source 18 | id: source 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup golang 22 | id: golang 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: ^1.23.0 26 | 27 | - name: Run generate 28 | id: generate 29 | run: make generate 30 | 31 | - name: Run vet 32 | id: vet 33 | run: make vet 34 | 35 | - name: Run staticcheck 36 | id: staticcheck 37 | run: make staticcheck 38 | 39 | - name: Run lint 40 | id: lint 41 | run: make lint 42 | 43 | - name: Run build 44 | id: build 45 | run: make build 46 | 47 | - name: Run test 48 | id: test 49 | run: make test 50 | 51 | - name: Coverage report 52 | id: codacy 53 | if: github.ref == 'refs/heads/master' 54 | uses: codacy/codacy-coverage-reporter-action@v1 55 | with: 56 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 57 | coverage-reports: coverage.out 58 | force-coverage-parser: go 59 | 60 | ... 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .direnv 2 | .devenv 3 | 4 | coverage.out 5 | 6 | /bin 7 | /dist 8 | 9 | /storage 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for unreleased 2 | 3 | The following sections list the changes for unreleased. 4 | 5 | ## Summary 6 | 7 | * Chg #28: Integrate configuration files 8 | 9 | ## Details 10 | 11 | * Change #28: Integrate configuration files 12 | 13 | We integrated the functionality to support different kinds of configuration 14 | files. You can find example configurations within the repository. The supported 15 | file formats are pretty flexible, so far it should work out of the box with 16 | `yaml`, `json` and at least `hcl`. 17 | 18 | https://github.com/webhippie/terrastate/issues/28 19 | 20 | 21 | # Changelog for 1.0.1 22 | 23 | The following sections list the changes for 1.0.1. 24 | 25 | ## Summary 26 | 27 | * Fix #30: Bind flags correctly to variables 28 | 29 | ## Details 30 | 31 | * Bugfix #30: Bind flags correctly to variables 32 | 33 | We fixed the binding of flags to variables as this had been bound to the root 34 | command instead of the server command where it belongs to. 35 | 36 | https://github.com/webhippie/terrastate/issues/30 37 | 38 | 39 | # Changelog for 1.0.0 40 | 41 | The following sections list the changes for 1.0.0. 42 | 43 | ## Summary 44 | 45 | * Chg #3: Initial release of basic version 46 | 47 | ## Details 48 | 49 | * Change #3: Initial release of basic version 50 | 51 | Just prepared an initial basic version which could be released to the public. 52 | 53 | https://github.com/webhippie/terrastate/issues/3 54 | 55 | 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Webhippie 2 | 3 | Welcome! Our community focuses on helping others and making this project the 4 | best it can be. We gladly accept contributions and encourage you to get 5 | involved! 6 | 7 | ## Bug reports 8 | 9 | Please search the issues on the issue tracker with a variety of keywords to 10 | ensure your bug is not already reported. 11 | 12 | If unique, [open an issue][issues] and 13 | answer the questions so we can understand and reproduce the problematic 14 | behavior. 15 | 16 | The burden is on you to convince us that it is actually a bug in our project. 17 | This is easiest to do when you write clear, concise instructions so we can 18 | reproduce the behavior (even if it seems obvious). The more detailed and 19 | specific you are, the faster we will be able to help you. Check out 20 | [How to Report Bugs Effectively][bugreport]. 21 | 22 | Please be kind, remember that this project comes at no cost to you, and you're 23 | getting free help. 24 | 25 | ## Check for assigned people 26 | 27 | We are using Github Issues for submitting known issues, e.g. bugs, features, 28 | etc. Some issues will have someone assigned, meaning that there's already 29 | someone that takes responsability for fixing said issue. This is not done to 30 | discourage contributions, rather to not step in the work that has already been 31 | done by the assignee. If you want to work on a known issue with someone already 32 | assigned to it, please consider contacting the assignee first, e.g. by 33 | mentioning the assignee in a new comment on the specific issue. This way you can 34 | contribute with ideas, or even with code if the assignee decides that you can 35 | step in. 36 | 37 | If you plan to work on a non assigned issue, please add a comment on the issue 38 | to prevent duplicated work. 39 | 40 | ## Minor improvements and new tests 41 | 42 | Submit pull requests at any time for minor changes or new tests. Make sure to 43 | write tests to assert your change is working properly and is thoroughly covered. 44 | We'll ask most pull requests to be squashed, especially with small commits. 45 | 46 | Your pull request may be thoroughly reviewed. This is because if we accept the 47 | PR, we also assume responsibility for it, although we would prefer you to help 48 | maintain your code after it gets merged. 49 | 50 | ## Mind the Style 51 | 52 | We believe that in order to have a healthy codebase we need to abide to a 53 | certain code style. We use `gofmt` with Go and `eslint` with Javscript for this 54 | matter, which are tools that has proved to be useful. So, before submitting your 55 | pull request, make sure that `gofmt` and if viable `eslint` are passing for you. 56 | 57 | Finally, note that `gofmt` and if viable `eslint` are called on the CI system. 58 | This means that your pull request will not be merged until the changes are 59 | approved. 60 | 61 | ## Update the Changelog 62 | 63 | We keep a changelog in the `CHANGELOG.md` file. This is useful to understand 64 | what has changed between each version. When you implement a new feature, or a 65 | fix for an issue, please also update the `CHANGELOG.md` file accordingly. We 66 | don't follow a strict style for the changelog, just try to be consistent with 67 | the rest of the file. 68 | 69 | ## Sign your work 70 | 71 | The sign-off is a simple line at the end of the explanation for the patch. Your 72 | signature certifies that you wrote the patch or otherwise have the right to pass 73 | it on as an open-source patch. The rules are pretty simple: If you can certify 74 | [DCO](./DCO), then you just add a line to every git commit message: 75 | 76 | ```console 77 | Signed-off-by: Joe Smith 78 | ``` 79 | 80 | Please use your real name, we really dislike pseudonyms or anonymous 81 | contributions. We are in the opensource world without secrets. If you set your 82 | `user.name` and `user.email` git configs, you can sign your commit automatically 83 | with `git commit -s`. 84 | 85 | ## Collaborator status 86 | 87 | If your pull request is merged, congratulations! You're technically a 88 | collaborator. We may also grant you "collaborator status" which means you can 89 | push to the repository and merge other pull requests. We hope that you will stay 90 | involved by reviewing pull requests, submitting more of your own, and resolving 91 | issues as you are able to. Thanks for making this project amazing! 92 | 93 | We ask that collaborators will conduct thorough code reviews and be nice to new 94 | contributors. Before merging a PR, it's best to get the approval of at least one 95 | or two other collaborators and/or the project owner. We prefer squashed commits 96 | instead of many little, semantically-unimportant commits. Also, CI and other 97 | post-commit hooks must pass before being merged except in certain unusual 98 | circumstances. 99 | 100 | Collaborator status may be removed for inactive users from time to time as we 101 | see fit; this is not an insult, just a basic security precaution in case the 102 | account becomes inactive or abandoned. Privileges can always be restored later. 103 | 104 | **Reviewing pull requests:** Please help submit and review pull requests as you 105 | are able! We would ask that every pull request be reviewed by at least one 106 | collaborator who did not open the pull request before merging. This will help 107 | ensure high code quality as new collaborators are added to the project. 108 | 109 | ## Vulnerabilities 110 | 111 | If you've found a vulnerability that is serious, please email to 112 | gopad@webhippie.de. If it's not a big deal, a pull request will probably be 113 | faster. 114 | 115 | ## Thank you 116 | 117 | Thanks for your help! This project would not be what it is today without your 118 | contributions. 119 | 120 | [issues]: https://github.com/webhippie/terrastate/issues 121 | [bugreport]: http://www.chiark.greenend.org.uk/~sgtatham/bugs.html 122 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .bingo/Variables.mk 2 | 3 | SHELL := bash 4 | NAME := terrastate 5 | IMPORT := github.com/webhippie/$(NAME) 6 | BIN := bin 7 | DIST := dist 8 | 9 | ifeq ($(OS), Windows_NT) 10 | EXECUTABLE := $(NAME).exe 11 | UNAME := Windows 12 | else 13 | EXECUTABLE := $(NAME) 14 | UNAME := $(shell uname -s) 15 | endif 16 | 17 | GOBUILD ?= CGO_ENABLED=0 go build 18 | PACKAGES ?= $(shell go list ./...) 19 | SOURCES ?= $(shell find . -name "*.go" -type f -not -path ./.devenv/\* -not -path ./.direnv/\*) 20 | GENERATE ?= $(PACKAGES) 21 | 22 | TAGS ?= netgo 23 | 24 | ifndef OUTPUT 25 | ifeq ($(GITHUB_REF_TYPE), tag) 26 | OUTPUT ?= $(subst v,,$(GITHUB_REF_NAME)) 27 | else 28 | OUTPUT ?= testing 29 | endif 30 | endif 31 | 32 | ifndef VERSION 33 | ifeq ($(GITHUB_REF_TYPE), tag) 34 | VERSION ?= $(subst v,,$(GITHUB_REF_NAME)) 35 | else 36 | VERSION ?= $(shell git rev-parse --short HEAD) 37 | endif 38 | endif 39 | 40 | ifndef DATE 41 | DATE := $(shell date -u '+%Y%m%d') 42 | endif 43 | 44 | ifndef SHA 45 | SHA := $(shell git rev-parse --short HEAD) 46 | endif 47 | 48 | LDFLAGS += -s -w -extldflags "-static" -X "$(IMPORT)/pkg/version.String=$(VERSION)" -X "$(IMPORT)/pkg/version.Revision=$(SHA)" -X "$(IMPORT)/pkg/version.Date=$(DATE)" 49 | GCFLAGS += all=-N -l 50 | 51 | .PHONY: all 52 | all: build 53 | 54 | .PHONY: sync 55 | sync: 56 | go mod download 57 | 58 | .PHONY: clean 59 | clean: 60 | go clean -i ./... 61 | rm -rf $(BIN) $(DIST) 62 | 63 | .PHONY: fmt 64 | fmt: 65 | gofmt -s -w $(SOURCES) 66 | 67 | .PHONY: vet 68 | vet: 69 | go vet $(PACKAGES) 70 | 71 | .PHONY: staticcheck 72 | staticcheck: $(STATICCHECK) 73 | $(STATICCHECK) -tags '$(TAGS)' $(PACKAGES) 74 | 75 | .PHONY: lint 76 | lint: $(REVIVE) 77 | for PKG in $(PACKAGES); do $(REVIVE) -config revive.toml -set_exit_status $$PKG || exit 1; done; 78 | 79 | .PHONY: generate 80 | generate: 81 | go generate $(GENERATE) 82 | 83 | .PHONY: changelog 84 | changelog: $(CALENS) 85 | $(CALENS) >| CHANGELOG.md 86 | 87 | .PHONY: test 88 | test: 89 | go test -coverprofile coverage.out $(PACKAGES) 90 | 91 | .PHONY: install 92 | install: $(SOURCES) 93 | go install -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/$(NAME) 94 | 95 | .PHONY: build 96 | build: $(BIN)/$(EXECUTABLE) 97 | 98 | $(BIN)/$(EXECUTABLE): $(SOURCES) 99 | $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 100 | 101 | $(BIN)/$(EXECUTABLE)-debug: $(SOURCES) 102 | $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -gcflags '$(GCFLAGS)' -o $@ ./cmd/$(NAME) 103 | 104 | .PHONY: release 105 | release: $(DIST) release-linux release-darwin release-windows release-checksum 106 | 107 | $(DIST): 108 | mkdir -p $(DIST) 109 | 110 | .PHONY: release-linux 111 | release-linux: $(DIST) \ 112 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-386 \ 113 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-amd64 \ 114 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-arm-5 \ 115 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-arm-6 \ 116 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-arm-7 \ 117 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-arm64 \ 118 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-mips \ 119 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-mips64 \ 120 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-mipsle \ 121 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-mips64le 122 | 123 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-386: 124 | GOOS=linux GOARCH=386 $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 125 | 126 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-amd64: 127 | GOOS=linux GOARCH=amd64 $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 128 | 129 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-arm-5: 130 | GOOS=linux GOARCH=arm GOARM=5 $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 131 | 132 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-arm-6: 133 | GOOS=linux GOARCH=arm GOARM=6 $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 134 | 135 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-arm-7: 136 | GOOS=linux GOARCH=arm GOARM=7 $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 137 | 138 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-arm64: 139 | GOOS=linux GOARCH=arm64 $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 140 | 141 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-mips: 142 | GOOS=linux GOARCH=mips $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 143 | 144 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-mips64: 145 | GOOS=linux GOARCH=mips64 $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 146 | 147 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-mipsle: 148 | GOOS=linux GOARCH=mipsle $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 149 | 150 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-linux-mips64le: 151 | GOOS=linux GOARCH=mips64le $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 152 | 153 | .PHONY: release-darwin 154 | release-darwin: $(DIST) \ 155 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-darwin-amd64 \ 156 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-darwin-arm64 157 | 158 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-darwin-amd64: 159 | GOOS=darwin GOARCH=amd64 $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 160 | 161 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-darwin-arm64: 162 | GOOS=darwin GOARCH=arm64 $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 163 | 164 | .PHONY: release-windows 165 | release-windows: $(DIST) \ 166 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-windows-4.0-386.exe \ 167 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-windows-4.0-amd64.exe 168 | 169 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-windows-4.0-386.exe: 170 | GOOS=windows GOARCH=386 $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 171 | 172 | $(DIST)/$(EXECUTABLE)-$(OUTPUT)-windows-4.0-amd64.exe: 173 | GOOS=windows GOARCH=amd64 $(GOBUILD) -v -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $@ ./cmd/$(NAME) 174 | 175 | .PHONY: release-reduce 176 | release-reduce: 177 | cd $(DIST); $(foreach file,$(wildcard $(DIST)/$(EXECUTABLE)-*),upx $(notdir $(file));) 178 | 179 | .PHONY: release-checksum 180 | release-checksum: 181 | cd $(DIST); $(foreach file,$(wildcard $(DIST)/$(EXECUTABLE)-*),sha256sum $(notdir $(file)) > $(notdir $(file)).sha256;) 182 | 183 | .PHONY: release-finish 184 | release-finish: release-reduce release-checksum 185 | 186 | .PHONY: docs 187 | docs: 188 | hugo -s docs/ 189 | 190 | .PHONY: watch 191 | watch: $(REFLEX) 192 | $(REFLEX) -c reflex.conf 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terrastate 2 | 3 | [![Current Tag](https://img.shields.io/github/v/tag/webhippie/terrastate?sort=semver)](https://github.com/webhippie/terrastate) [![Build Status](https://github.com/webhippie/terrastate/actions/workflows/general.yml/badge.svg)](https://github.com/webhippie/terrastate/actions) [![Join the Matrix chat at https://matrix.to/#/#webhippie:matrix.org](https://img.shields.io/badge/matrix-%23webhippie-7bc9a4.svg)](https://matrix.to/#/#webhippie:matrix.org) [![Go Reference](https://pkg.go.dev/badge/github.com/webhippie/terrastate.svg)](https://pkg.go.dev/github.com/webhippie/terrastate) [![Go Report Card](https://goreportcard.com/badge/github.com/webhippie/terrastate)](https://goreportcard.com/report/github.com/webhippie/terrastate) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/d2bc4877341f4c7fbf9b4fa62b8d0484)](https://www.codacy.com/gh/webhippie/terrastate/dashboard?utm_source=github.com&utm_medium=referral&utm_content=webhippie/terrastate&utm_campaign=Badge_Grade) 4 | 5 | Terrastate acts as an HTTP backend for Terraform which can store the state 6 | content remotely for you to keep it outside of the repositories containing your 7 | `.tf` files. This is a great alternative if you are not hosting your stuff on 8 | AWS. 9 | 10 | ## Install 11 | 12 | You can download prebuilt binaries from our [GitHub releases][releases] or our 13 | [download mirror][downloads]. Beside that we are publishing Docker images to 14 | [Docker Hub][dockerhub] and [Quay][quay]. If you need further guidance how to 15 | install this take a look at our [documentation][docs]. 16 | 17 | ## Development 18 | 19 | Make sure you have a working Go environment, for further reference or a guide 20 | take a look at the [install instructions][golang]. This project requires 21 | Go >= v1.17, at least that's the version we are using. 22 | 23 | ```console 24 | git clone https://github.com/webhippie/terrastate.git 25 | cd terrastate 26 | 27 | make generate build 28 | 29 | ./bin/terrastate -h 30 | ``` 31 | 32 | ## Security 33 | 34 | If you find a security issue please contact 35 | [thomas@webhippie.de](mailto:thomas@webhippie.de) first. 36 | 37 | ## Contributing 38 | 39 | Fork -> Patch -> Push -> Pull Request 40 | 41 | ## Authors 42 | 43 | - [Thomas Boerger](https://github.com/tboerger) 44 | 45 | ## License 46 | 47 | Apache-2.0 48 | 49 | ## Copyright 50 | 51 | ```console 52 | Copyright (c) 2018 Thomas Boerger 53 | ``` 54 | 55 | [releases]: https://github.com/webhippie/terrastate/releases 56 | [downloads]: https://dl.webhippie.de/#terrastate/ 57 | [dockerhub]: https://hub.docker.com/r/webhippie/terrastate/tags/ 58 | [quay]: https://quay.io/repository/webhippie/terrastate?tab=tags 59 | [docs]: https://webhippie.github.io/terrastate/#getting-started 60 | [golang]: http://golang.org/doc/install.html 61 | -------------------------------------------------------------------------------- /changelog/1.0.0_2022-05-05/initial-release.md: -------------------------------------------------------------------------------- 1 | Change: Initial release of basic version 2 | 3 | Just prepared an initial basic version which could be released to the public. 4 | 5 | https://github.com/webhippie/terrastate/issues/3 6 | -------------------------------------------------------------------------------- /changelog/1.0.1_2022-05-10/fix-bindings.md: -------------------------------------------------------------------------------- 1 | Bugfix: Bind flags correctly to variables 2 | 3 | We fixed the binding of flags to variables as this had been bound to the root 4 | command instead of the server command where it belongs to. 5 | 6 | https://github.com/webhippie/terrastate/issues/30 7 | -------------------------------------------------------------------------------- /changelog/CHANGELOG.tmpl: -------------------------------------------------------------------------------- 1 | {{- range $changes := . }}{{ with $changes -}} 2 | # Changelog for {{ .Version }} 3 | 4 | The following sections list the changes for {{ .Version }}. 5 | 6 | ## Summary 7 | {{ range $entry := .Entries }}{{ with $entry }} 8 | * {{ .TypeShort }} #{{ .PrimaryID }}: {{ .Title }} 9 | {{- end }}{{ end }} 10 | 11 | ## Details 12 | {{ range $entry := .Entries }}{{ with $entry }} 13 | * {{ .Type }} #{{ .PrimaryID }}: {{ .Title }} 14 | {{ range $par := .Paragraphs }} 15 | {{ wrapIndent $par 80 3 }} 16 | {{ end -}} 17 | {{ range $url := .IssueURLs }} 18 | {{ $url -}} 19 | {{ end -}} 20 | {{ range $url := .PRURLs }} 21 | {{ $url -}} 22 | {{ end -}} 23 | {{ range $url := .OtherURLs }} 24 | {{ $url -}} 25 | {{ end }} 26 | {{ end }}{{ end }} 27 | 28 | {{ end }}{{ end -}} 29 | -------------------------------------------------------------------------------- /changelog/README.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | We are using [calens](https://github.com/restic/calens) to properly generate a 4 | changelog before we are tagging a new release. To get an idea how this could 5 | look like would be the 6 | best reference. 7 | -------------------------------------------------------------------------------- /changelog/TEMPLATE: -------------------------------------------------------------------------------- 1 | Bugfix: Fix behavior for foobar (in present tense) 2 | 3 | We've fixed the behavior for foobar, a long-standing annoyance for users. The 4 | text should be wrapped at 80 characters length. 5 | 6 | The text in the paragraphs is written in past tense. The last section is a list 7 | of issue URLs, PR URLs and other URLs. The first issue ID (or the first PR ID, 8 | in case there aren't any issue links) is used as the primary ID. 9 | 10 | https://github.com/webhippie/terrastate/issues/1234 11 | https://github.com/webhippie/terrastate/pull/55555 12 | -------------------------------------------------------------------------------- /changelog/unreleased/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webhippie/terrastate/d7977675b10c8163df56580962e75a0c7fc0d02b/changelog/unreleased/.keep -------------------------------------------------------------------------------- /changelog/unreleased/config-file.md: -------------------------------------------------------------------------------- 1 | Change: Integrate configuration files 2 | 3 | We integrated the functionality to support different kinds of configuration 4 | files. You can find example configurations within the repository. The supported 5 | file formats are pretty flexible, so far it should work out of the box with 6 | `yaml`, `json` and at least `hcl`. 7 | 8 | https://github.com/webhippie/terrastate/issues/28 9 | -------------------------------------------------------------------------------- /cmd/terrastate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/joho/godotenv" 7 | "github.com/webhippie/terrastate/pkg/command" 8 | ) 9 | 10 | func main() { 11 | if env := os.Getenv("TERRASTATE_ENV_FILE"); env != "" { 12 | godotenv.Load(env) 13 | } 14 | 15 | if err := command.Run(); err != nil { 16 | os.Exit(1) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /config/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "addr": "0.0.0.0:8080", 4 | "host": "http://localhost:8080", 5 | "pprof": false, 6 | "root": "/", 7 | "cert": "", 8 | "key": "", 9 | "strict_curves": false, 10 | "strict_ciphers": false, 11 | "storage": "storage/" 12 | }, 13 | "metrics": { 14 | "addr": "0.0.0.0:8081", 15 | "token": "" 16 | }, 17 | "log": { 18 | "level": "info", 19 | "pretty": true, 20 | "color": true 21 | }, 22 | "encryption": { 23 | "secret": "" 24 | }, 25 | "access": { 26 | "username": "", 27 | "password": "" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /config/example.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | server: 3 | addr: 0.0.0.0:8080 4 | host: http://localhost:8080 5 | pprof: false 6 | root: / 7 | cert: 8 | key: 9 | strict_curves: false 10 | strict_ciphers: false 11 | storage: storage/ 12 | 13 | metrics: 14 | addr: 0.0.0.0:8081 15 | token: 16 | 17 | log: 18 | level: info 19 | pretty: true 20 | color: true 21 | 22 | encryption: 23 | secret: 24 | 25 | access: 26 | username: 27 | password: 28 | 29 | ... 30 | -------------------------------------------------------------------------------- /docker/Dockerfile.linux.386: -------------------------------------------------------------------------------- 1 | FROM i386/alpine:3.22@sha256:dcfdb8bfec3218e0d2e402265f965bc241871392b0b686796137d63cead3945b AS build 2 | RUN apk add --no-cache ca-certificates mailcap 3 | 4 | FROM scratch 5 | 6 | EXPOSE 8080 8081 7 | VOLUME ["/var/lib/terrastate"] 8 | ENTRYPOINT ["/usr/bin/terrastate"] 9 | CMD ["server"] 10 | 11 | ENV TERRASTATE_STORAGE /var/lib/terrastate 12 | 13 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 14 | COPY --from=build /etc/mime.types /etc/ 15 | 16 | COPY bin/terrastate /usr/bin/terrastate 17 | -------------------------------------------------------------------------------- /docker/Dockerfile.linux.amd64: -------------------------------------------------------------------------------- 1 | FROM amd64/alpine:3.22@sha256:f29909b294ed398ae64ad9bc268a3ce2c4824fb37375cac63763e6e8f886f3b1 AS build 2 | RUN apk add --no-cache ca-certificates mailcap 3 | 4 | FROM scratch 5 | 6 | EXPOSE 8080 8081 7 | VOLUME ["/var/lib/terrastate"] 8 | ENTRYPOINT ["/usr/bin/terrastate"] 9 | CMD ["server"] 10 | 11 | ENV TERRASTATE_STORAGE /var/lib/terrastate 12 | 13 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 14 | COPY --from=build /etc/mime.types /etc/ 15 | 16 | COPY bin/terrastate /usr/bin/terrastate 17 | -------------------------------------------------------------------------------- /docker/Dockerfile.linux.arm: -------------------------------------------------------------------------------- 1 | FROM arm32v6/alpine:3.22@sha256:1b418ed7e714de83d1340852aa0127c4b0a20f70dd4af970e452a2dc06979f98 AS build 2 | RUN apk add --no-cache ca-certificates mailcap 3 | 4 | FROM scratch 5 | 6 | EXPOSE 8080 8081 7 | VOLUME ["/var/lib/terrastate"] 8 | ENTRYPOINT ["/usr/bin/terrastate"] 9 | CMD ["server"] 10 | 11 | ENV TERRASTATE_STORAGE /var/lib/terrastate 12 | 13 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 14 | COPY --from=build /etc/mime.types /etc/ 15 | 16 | COPY bin/terrastate /usr/bin/terrastate 17 | -------------------------------------------------------------------------------- /docker/Dockerfile.linux.arm64: -------------------------------------------------------------------------------- 1 | FROM arm64v8/alpine:3.22@sha256:fa4cf50559eaaaafd69341a3bc5fc09047b53480c884a3bc3e4f6e13da13f503 AS build 2 | RUN apk add --no-cache ca-certificates mailcap 3 | 4 | FROM scratch 5 | 6 | EXPOSE 8080 8081 7 | VOLUME ["/var/lib/terrastate"] 8 | ENTRYPOINT ["/usr/bin/terrastate"] 9 | CMD ["server"] 10 | 11 | ENV TERRASTATE_STORAGE /var/lib/terrastate 12 | 13 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 14 | COPY --from=build /etc/mime.types /etc/ 15 | 16 | COPY bin/terrastate /usr/bin/terrastate 17 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | public/ 2 | resources/ 3 | 4 | .hugo_build.lock 5 | -------------------------------------------------------------------------------- /docs/archetypes/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ replace .TranslationBaseName "-" " " | title }}" 3 | date: {{ .Date }} 4 | anchor: "{{ replace .TranslationBaseName "-" " " | title | urlize }}" 5 | weight: 6 | --- 7 | -------------------------------------------------------------------------------- /docs/config.toml: -------------------------------------------------------------------------------- 1 | baseURL = "https://webhippie.github.io/terrastate/" 2 | languageCode = "en-us" 3 | title = "Terrastate" 4 | pygmentsUseClasses = true 5 | 6 | [markup.goldmark.renderer] 7 | unsafe = true 8 | 9 | [blackfriday] 10 | angledQuotes = true 11 | fractions = false 12 | plainIDAnchors = true 13 | smartlists = true 14 | extensions = ["hardLineBreak"] 15 | 16 | [params] 17 | author = "Thomas Boerger" 18 | description = "Terraform HTTP remote state storage" 19 | keywords = "terraform, remote, state, storage" 20 | -------------------------------------------------------------------------------- /docs/content/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "About" 3 | date: 2022-05-04T00:00:00+00:00 4 | anchor: "about" 5 | weight: 10 6 | --- 7 | 8 | This service provides the Terraform Remote State API. You don't need to store 9 | your Terraform state within S3 or some other service similar to that, you can 10 | just use this small service on your own infrastructure. 11 | -------------------------------------------------------------------------------- /docs/content/building.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Building" 3 | date: 2022-05-03T00:00:00+00:00 4 | anchor: "building" 5 | weight: 30 6 | --- 7 | 8 | As this project is built with Go you need to install Go first. The installation 9 | of Go is out of the scope of this document, please follow the 10 | [official documentation][golang]. After the installation of Go you need to get 11 | the sources: 12 | 13 | {{< highlight txt >}} 14 | git clone https://github.com/webhippie/terrastate.git 15 | cd terrastate/ 16 | {{< / highlight >}} 17 | 18 | All required tool besides Go itself are bundled by Go modules, all you need is 19 | part of the `Makfile`: 20 | 21 | {{< highlight txt >}} 22 | make generate build 23 | {{< / highlight >}} 24 | 25 | Finally you should have the binary within the `bin/` folder now, give it a try 26 | with `./bin/terrastate -h` to see all available options. 27 | 28 | [golang]: https://golang.org/doc/install 29 | -------------------------------------------------------------------------------- /docs/content/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started" 3 | date: 2022-05-04T00:00:00+00:00 4 | anchor: "getting-started" 5 | weight: 20 6 | --- 7 | 8 | ## Installation 9 | 10 | So far we are offering only a few different variants for the installation. You 11 | can choose between [Docker][docker] or pre-built binaries which are stored on 12 | our download mirror and GitHub releases. Maybe we will also provide system 13 | packages for the major distributions later if we see the need for it. 14 | 15 | ### Docker 16 | 17 | Generally we are offering the images through 18 | [quay.io/webhippie/terrastate][quay] and [webhippie/terrastate][dockerhub], so 19 | feel free to choose one of the providers. Maybe we will come up with Kustomize 20 | manifests or some Helm chart. 21 | 22 | ### Binaries 23 | 24 | Simply download a binary matching your operating system and your architecture 25 | from our [downloads][downloads] or the GitHub releases and place it within your 26 | path like `/usr/local/bin` if you are using macOS or Linux. 27 | 28 | ## Configuration 29 | 30 | We provide overall three different variants of configuration. The variant based 31 | on environment variables and commandline flags are split up into global values 32 | and command-specific values. 33 | 34 | ### Envrionment variables 35 | 36 | If you prefer to configure the service with environment variables you can see 37 | the available variables below. 38 | 39 | #### Global 40 | 41 | TERRASTATE_CONFIG_FILE 42 | : Path to optional config file 43 | 44 | TERRASTATE_LOG_LEVEL 45 | : Set logging level, defaults to `info` 46 | 47 | TERRASTATE_LOG_COLOR 48 | : Enable colored logging, defaults to `true` 49 | 50 | TERRASTATE_LOG_PRETTY 51 | : Enable pretty logging, defaults to `true` 52 | 53 | #### Server 54 | 55 | TERRASTATE_METRICS_ADDR 56 | : Address to bind the metrics, defaults to `0.0.0.0:8081` 57 | 58 | TERRASTATE_METRICS_TOKEN 59 | : Token to make metrics secure 60 | 61 | TERRASTATE_SERVER_ADDR 62 | : Address to bind the server, defaults to `0.0.0.0:8080` 63 | 64 | TERRASTATE_SERVER_PPROF 65 | : Enable pprof debugging, defaults to `false` 66 | 67 | TERRASTATE_SERVER_ROOT 68 | : Root path of the server, defaults to `/` 69 | 70 | TERRASTATE_SERVER_HOST 71 | : External access to server, defaults to `http://localhost:8080` 72 | 73 | TERRASTATE_SERVER_CERT 74 | : Path to cert for SSL encryption 75 | 76 | TERRASTATE_SERVER_KEY 77 | : Path to key for SSL encryption 78 | 79 | TERRASTATE_SERVER_STRICT_CURVES 80 | : Use strict SSL curves, defaults to `false` 81 | 82 | TERRASTATE_SERVER_STRICT_CIPHERS 83 | : Use strict SSL ciphers, defaults to `false` 84 | 85 | TERRASTATE_SERVER_STORAGE 86 | : Folder for storing the states, defaults to `storage/` 87 | 88 | TERRASTATE_ENCRYPTION_SECRET 89 | : Secret for file encryption 90 | 91 | TERRASTATE_ACCESS_USERNAME 92 | : Username for basic auth 93 | 94 | TERRASTATE_ACCESS_PASSWORD 95 | : Password for basic auth 96 | 97 | #### Health 98 | 99 | TERRASTATE_METRICS_ADDR 100 | : Address to bind the metrics, defaults to `0.0.0.0:8081` 101 | 102 | #### State 103 | 104 | TERRASTATE_SERVER_STORAGE 105 | : Folder for storing the states, defaults to `storage/` 106 | 107 | TERRASTATE_ENCRYPTION_SECRET 108 | : Secret for file encryption 109 | 110 | ### Commandline flags 111 | 112 | If you prefer to configure the service with commandline flags you can see the 113 | available variables below. 114 | 115 | #### Global 116 | 117 | --config-file 118 | : Path to optional config file 119 | 120 | --log-level 121 | : Set logging level, defaults to `info` 122 | 123 | --log-color 124 | : Enable colored logging, defaults to `true` 125 | 126 | --log-pretty 127 | : Enable pretty logging, defaults to `true` 128 | 129 | #### Server 130 | 131 | --metrics-addr 132 | : Address to bind the metrics, defaults to `0.0.0.0:8081` 133 | 134 | --metrics-token 135 | : Token to make metrics secure 136 | 137 | --server-addr 138 | : Address to bind the server, defaults to `0.0.0.0:8080` 139 | 140 | --server-pprof 141 | : Enable pprof debugging, defaults to `false` 142 | 143 | --server-root 144 | : Root path of the server, defaults to `/` 145 | 146 | --server-host 147 | : External access to server, defaults to `http://localhost:8080` 148 | 149 | --server-cert 150 | : Path to cert for SSL encryption 151 | 152 | --server-key 153 | : Path to key for SSL encryption 154 | 155 | --strict-curves 156 | : Use strict SSL curves, defaults to `false` 157 | 158 | --strict-ciphers 159 | : Use strict SSL ciphers, defaults to `false` 160 | 161 | --storage-path 162 | : Folder for storing the states, defaults to `storage/` 163 | 164 | --encryption-secret 165 | : Secret for file encryption 166 | 167 | --general-username 168 | : Username for basic auth 169 | 170 | --general-password 171 | : Password for basic auth 172 | 173 | #### Health 174 | 175 | --metrics-addr 176 | : Address to bind the metrics, defaults to `0.0.0.0:8081` 177 | 178 | #### State 179 | 180 | --storage-path 181 | : Folder for storing the states, defaults to `storage/` 182 | 183 | --encryption-secret 184 | : Secret for file encryption 185 | 186 | ### Configuration file 187 | 188 | So far we support multiple file formats like `json`, `yaml`, `hcl` and possibly 189 | even more, if you want to get a full example configuration just take a look at 190 | [our repository][repo], there you can always see the latest configuration 191 | format. These example configs include all available options and the default 192 | values. The configuration file will be automatically loaded if it's placed at 193 | `/etc/terrastate/config.yml`, `${HOME}/.terrastate/config.yml` or 194 | `$(pwd)/terrastate/config.yml`. 195 | 196 | ## Usage 197 | 198 | The program provides a few sub-commands on execution. The available config 199 | methods have already been mentioned above. Generally you can always see a 200 | formated help output if you execute the binary similar to something like 201 | `terrastate --help`. 202 | 203 | [docker]: https://www.docker.com/ 204 | [quay]: https://quay.io/repository/webhippie/terrastate 205 | [dockerhub]: https://hub.docker.com/r/webhippie/terrastate 206 | [downloads]: https://dl.webhippie.de/#terrastate/ 207 | [repo]: https://github.com/webhippie/terrastate/tree/master/config 208 | -------------------------------------------------------------------------------- /docs/content/license.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "License" 3 | date: 2022-05-03T00:00:00+00:00 4 | anchor: "license" 5 | weight: 40 6 | --- 7 | 8 | This project is licensed under the [Apache 2.0][license] license. For the 9 | license of the used libraries you have to check the respective sources. 10 | 11 | [license]: https://github.com/webhippie/terrastate/blob/master/LICENSE 12 | -------------------------------------------------------------------------------- /docs/layouts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ .Site.Title }} 10 | 11 | 12 | 13 | 14 | 15 | {{ partial "style.html" . }} 16 | 17 | 18 | 19 | 38 | 39 | {{ range .Data.Pages.ByWeight }} 40 |
41 |

42 | 43 | {{ .Title }} 44 | 45 | 46 | 47 | 48 | Back to Top 49 | 50 | 51 |

52 | 53 | {{ .Content | markdownify }} 54 |
55 | {{ end }} 56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/layouts/partials/style.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 339 | -------------------------------------------------------------------------------- /docs/layouts/shortcodes/partial.html: -------------------------------------------------------------------------------- 1 | {{- $file := printf "/partials/%s" (.Get 0) -}}{{- $file | readFile | markdownify -}} 2 | -------------------------------------------------------------------------------- /docs/partials/envvars.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webhippie/terrastate/d7977675b10c8163df56580962e75a0c7fc0d02b/docs/partials/envvars.md -------------------------------------------------------------------------------- /docs/static/syntax.css: -------------------------------------------------------------------------------- 1 | /* Background */ .chroma { color: #f8f8f2; background-color: #272822 } 2 | /* Error */ .chroma .err { color: #960050; background-color: #1e0010 } 3 | /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 4 | /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } 5 | /* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } 6 | /* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; } 7 | /* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; } 8 | /* Keyword */ .chroma .k { color: #66d9ef } 9 | /* KeywordConstant */ .chroma .kc { color: #66d9ef } 10 | /* KeywordDeclaration */ .chroma .kd { color: #66d9ef } 11 | /* KeywordNamespace */ .chroma .kn { color: #f92672 } 12 | /* KeywordPseudo */ .chroma .kp { color: #66d9ef } 13 | /* KeywordReserved */ .chroma .kr { color: #66d9ef } 14 | /* KeywordType */ .chroma .kt { color: #66d9ef } 15 | /* NameAttribute */ .chroma .na { color: #a6e22e } 16 | /* NameClass */ .chroma .nc { color: #a6e22e } 17 | /* NameConstant */ .chroma .no { color: #66d9ef } 18 | /* NameDecorator */ .chroma .nd { color: #a6e22e } 19 | /* NameException */ .chroma .ne { color: #a6e22e } 20 | /* NameFunction */ .chroma .nf { color: #a6e22e } 21 | /* NameOther */ .chroma .nx { color: #a6e22e } 22 | /* NameTag */ .chroma .nt { color: #f92672 } 23 | /* Literal */ .chroma .l { color: #ae81ff } 24 | /* LiteralDate */ .chroma .ld { color: #e6db74 } 25 | /* LiteralString */ .chroma .s { color: #e6db74 } 26 | /* LiteralStringAffix */ .chroma .sa { color: #e6db74 } 27 | /* LiteralStringBacktick */ .chroma .sb { color: #e6db74 } 28 | /* LiteralStringChar */ .chroma .sc { color: #e6db74 } 29 | /* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 } 30 | /* LiteralStringDoc */ .chroma .sd { color: #e6db74 } 31 | /* LiteralStringDouble */ .chroma .s2 { color: #e6db74 } 32 | /* LiteralStringEscape */ .chroma .se { color: #ae81ff } 33 | /* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 } 34 | /* LiteralStringInterpol */ .chroma .si { color: #e6db74 } 35 | /* LiteralStringOther */ .chroma .sx { color: #e6db74 } 36 | /* LiteralStringRegex */ .chroma .sr { color: #e6db74 } 37 | /* LiteralStringSingle */ .chroma .s1 { color: #e6db74 } 38 | /* LiteralStringSymbol */ .chroma .ss { color: #e6db74 } 39 | /* LiteralNumber */ .chroma .m { color: #ae81ff } 40 | /* LiteralNumberBin */ .chroma .mb { color: #ae81ff } 41 | /* LiteralNumberFloat */ .chroma .mf { color: #ae81ff } 42 | /* LiteralNumberHex */ .chroma .mh { color: #ae81ff } 43 | /* LiteralNumberInteger */ .chroma .mi { color: #ae81ff } 44 | /* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff } 45 | /* LiteralNumberOct */ .chroma .mo { color: #ae81ff } 46 | /* Operator */ .chroma .o { color: #f92672 } 47 | /* OperatorWord */ .chroma .ow { color: #f92672 } 48 | /* Comment */ .chroma .c { color: #75715e } 49 | /* CommentHashbang */ .chroma .ch { color: #75715e } 50 | /* CommentMultiline */ .chroma .cm { color: #75715e } 51 | /* CommentSingle */ .chroma .c1 { color: #75715e } 52 | /* CommentSpecial */ .chroma .cs { color: #75715e } 53 | /* CommentPreproc */ .chroma .cp { color: #75715e } 54 | /* CommentPreprocFile */ .chroma .cpf { color: #75715e } 55 | /* GenericDeleted */ .chroma .gd { color: #f92672 } 56 | /* GenericEmph */ .chroma .ge { font-style: italic } 57 | /* GenericInserted */ .chroma .gi { color: #a6e22e } 58 | /* GenericStrong */ .chroma .gs { font-weight: bold } 59 | /* GenericSubheading */ .chroma .gu { color: #75715e } 60 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "cachix": { 4 | "inputs": { 5 | "devenv": [ 6 | "devenv" 7 | ], 8 | "flake-compat": [ 9 | "devenv" 10 | ], 11 | "git-hooks": [ 12 | "devenv" 13 | ], 14 | "nixpkgs": "nixpkgs" 15 | }, 16 | "locked": { 17 | "lastModified": 1744206633, 18 | "narHash": "sha256-pb5aYkE8FOoa4n123slgHiOf1UbNSnKe5pEZC+xXD5g=", 19 | "owner": "cachix", 20 | "repo": "cachix", 21 | "rev": "8a60090640b96f9df95d1ab99e5763a586be1404", 22 | "type": "github" 23 | }, 24 | "original": { 25 | "owner": "cachix", 26 | "ref": "latest", 27 | "repo": "cachix", 28 | "type": "github" 29 | } 30 | }, 31 | "devenv": { 32 | "inputs": { 33 | "cachix": "cachix", 34 | "flake-compat": "flake-compat", 35 | "git-hooks": "git-hooks", 36 | "nix": "nix", 37 | "nixpkgs": "nixpkgs_3" 38 | }, 39 | "locked": { 40 | "lastModified": 1748795434, 41 | "narHash": "sha256-PMZ4qwBwMwDCpuE+CbhZMJDeY76HvweC4GdBNc7oh2U=", 42 | "owner": "cachix", 43 | "repo": "devenv", 44 | "rev": "02334cb7e3d69847e2ce793719147e63d56e2f7c", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "cachix", 49 | "repo": "devenv", 50 | "type": "github" 51 | } 52 | }, 53 | "flake-compat": { 54 | "flake": false, 55 | "locked": { 56 | "lastModified": 1733328505, 57 | "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 58 | "owner": "edolstra", 59 | "repo": "flake-compat", 60 | "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 61 | "type": "github" 62 | }, 63 | "original": { 64 | "owner": "edolstra", 65 | "repo": "flake-compat", 66 | "type": "github" 67 | } 68 | }, 69 | "flake-compat_2": { 70 | "flake": false, 71 | "locked": { 72 | "lastModified": 1696426674, 73 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 74 | "owner": "edolstra", 75 | "repo": "flake-compat", 76 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 77 | "type": "github" 78 | }, 79 | "original": { 80 | "owner": "edolstra", 81 | "repo": "flake-compat", 82 | "type": "github" 83 | } 84 | }, 85 | "flake-parts": { 86 | "inputs": { 87 | "nixpkgs-lib": [ 88 | "devenv", 89 | "nix", 90 | "nixpkgs" 91 | ] 92 | }, 93 | "locked": { 94 | "lastModified": 1712014858, 95 | "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", 96 | "owner": "hercules-ci", 97 | "repo": "flake-parts", 98 | "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", 99 | "type": "github" 100 | }, 101 | "original": { 102 | "owner": "hercules-ci", 103 | "repo": "flake-parts", 104 | "type": "github" 105 | } 106 | }, 107 | "flake-parts_2": { 108 | "inputs": { 109 | "nixpkgs-lib": "nixpkgs-lib" 110 | }, 111 | "locked": { 112 | "lastModified": 1748821116, 113 | "narHash": "sha256-F82+gS044J1APL0n4hH50GYdPRv/5JWm34oCJYmVKdE=", 114 | "owner": "hercules-ci", 115 | "repo": "flake-parts", 116 | "rev": "49f0870db23e8c1ca0b5259734a02cd9e1e371a1", 117 | "type": "github" 118 | }, 119 | "original": { 120 | "owner": "hercules-ci", 121 | "repo": "flake-parts", 122 | "type": "github" 123 | } 124 | }, 125 | "git-hooks": { 126 | "inputs": { 127 | "flake-compat": [ 128 | "devenv" 129 | ], 130 | "gitignore": "gitignore", 131 | "nixpkgs": [ 132 | "devenv", 133 | "nixpkgs" 134 | ] 135 | }, 136 | "locked": { 137 | "lastModified": 1746537231, 138 | "narHash": "sha256-Wb2xeSyOsCoTCTj7LOoD6cdKLEROyFAArnYoS+noCWo=", 139 | "owner": "cachix", 140 | "repo": "git-hooks.nix", 141 | "rev": "fa466640195d38ec97cf0493d6d6882bc4d14969", 142 | "type": "github" 143 | }, 144 | "original": { 145 | "owner": "cachix", 146 | "repo": "git-hooks.nix", 147 | "type": "github" 148 | } 149 | }, 150 | "gitignore": { 151 | "inputs": { 152 | "nixpkgs": [ 153 | "devenv", 154 | "git-hooks", 155 | "nixpkgs" 156 | ] 157 | }, 158 | "locked": { 159 | "lastModified": 1709087332, 160 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 161 | "owner": "hercules-ci", 162 | "repo": "gitignore.nix", 163 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 164 | "type": "github" 165 | }, 166 | "original": { 167 | "owner": "hercules-ci", 168 | "repo": "gitignore.nix", 169 | "type": "github" 170 | } 171 | }, 172 | "gitignore_2": { 173 | "inputs": { 174 | "nixpkgs": [ 175 | "pre-commit-hooks-nix", 176 | "nixpkgs" 177 | ] 178 | }, 179 | "locked": { 180 | "lastModified": 1709087332, 181 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 182 | "owner": "hercules-ci", 183 | "repo": "gitignore.nix", 184 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 185 | "type": "github" 186 | }, 187 | "original": { 188 | "owner": "hercules-ci", 189 | "repo": "gitignore.nix", 190 | "type": "github" 191 | } 192 | }, 193 | "libgit2": { 194 | "flake": false, 195 | "locked": { 196 | "lastModified": 1697646580, 197 | "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", 198 | "owner": "libgit2", 199 | "repo": "libgit2", 200 | "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", 201 | "type": "github" 202 | }, 203 | "original": { 204 | "owner": "libgit2", 205 | "repo": "libgit2", 206 | "type": "github" 207 | } 208 | }, 209 | "nix": { 210 | "inputs": { 211 | "flake-compat": [ 212 | "devenv" 213 | ], 214 | "flake-parts": "flake-parts", 215 | "libgit2": "libgit2", 216 | "nixpkgs": "nixpkgs_2", 217 | "nixpkgs-23-11": [ 218 | "devenv" 219 | ], 220 | "nixpkgs-regression": [ 221 | "devenv" 222 | ], 223 | "pre-commit-hooks": [ 224 | "devenv" 225 | ] 226 | }, 227 | "locked": { 228 | "lastModified": 1745930071, 229 | "narHash": "sha256-bYyjarS3qSNqxfgc89IoVz8cAFDkF9yPE63EJr+h50s=", 230 | "owner": "domenkozar", 231 | "repo": "nix", 232 | "rev": "b455edf3505f1bf0172b39a735caef94687d0d9c", 233 | "type": "github" 234 | }, 235 | "original": { 236 | "owner": "domenkozar", 237 | "ref": "devenv-2.24", 238 | "repo": "nix", 239 | "type": "github" 240 | } 241 | }, 242 | "nixpkgs": { 243 | "locked": { 244 | "lastModified": 1733212471, 245 | "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", 246 | "owner": "NixOS", 247 | "repo": "nixpkgs", 248 | "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", 249 | "type": "github" 250 | }, 251 | "original": { 252 | "owner": "NixOS", 253 | "ref": "nixos-unstable", 254 | "repo": "nixpkgs", 255 | "type": "github" 256 | } 257 | }, 258 | "nixpkgs-lib": { 259 | "locked": { 260 | "lastModified": 1748740939, 261 | "narHash": "sha256-rQaysilft1aVMwF14xIdGS3sj1yHlI6oKQNBRTF40cc=", 262 | "owner": "nix-community", 263 | "repo": "nixpkgs.lib", 264 | "rev": "656a64127e9d791a334452c6b6606d17539476e2", 265 | "type": "github" 266 | }, 267 | "original": { 268 | "owner": "nix-community", 269 | "repo": "nixpkgs.lib", 270 | "type": "github" 271 | } 272 | }, 273 | "nixpkgs_2": { 274 | "locked": { 275 | "lastModified": 1717432640, 276 | "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", 277 | "owner": "NixOS", 278 | "repo": "nixpkgs", 279 | "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", 280 | "type": "github" 281 | }, 282 | "original": { 283 | "owner": "NixOS", 284 | "ref": "release-24.05", 285 | "repo": "nixpkgs", 286 | "type": "github" 287 | } 288 | }, 289 | "nixpkgs_3": { 290 | "locked": { 291 | "lastModified": 1746807397, 292 | "narHash": "sha256-zU2z0jlkJGWLhdNr/8AJSxqK8XD0IlQgHp3VZcP56Aw=", 293 | "owner": "cachix", 294 | "repo": "devenv-nixpkgs", 295 | "rev": "c5208b594838ea8e6cca5997fbf784b7cca1ca90", 296 | "type": "github" 297 | }, 298 | "original": { 299 | "owner": "cachix", 300 | "ref": "rolling", 301 | "repo": "devenv-nixpkgs", 302 | "type": "github" 303 | } 304 | }, 305 | "nixpkgs_4": { 306 | "locked": { 307 | "lastModified": 1748693115, 308 | "narHash": "sha256-StSrWhklmDuXT93yc3GrTlb0cKSS0agTAxMGjLKAsY8=", 309 | "owner": "NixOS", 310 | "repo": "nixpkgs", 311 | "rev": "910796cabe436259a29a72e8d3f5e180fc6dfacc", 312 | "type": "github" 313 | }, 314 | "original": { 315 | "owner": "NixOS", 316 | "ref": "nixos-unstable", 317 | "repo": "nixpkgs", 318 | "type": "github" 319 | } 320 | }, 321 | "nixpkgs_5": { 322 | "locked": { 323 | "lastModified": 1730768919, 324 | "narHash": "sha256-8AKquNnnSaJRXZxc5YmF/WfmxiHX6MMZZasRP6RRQkE=", 325 | "owner": "NixOS", 326 | "repo": "nixpkgs", 327 | "rev": "a04d33c0c3f1a59a2c1cb0c6e34cd24500e5a1dc", 328 | "type": "github" 329 | }, 330 | "original": { 331 | "owner": "NixOS", 332 | "ref": "nixpkgs-unstable", 333 | "repo": "nixpkgs", 334 | "type": "github" 335 | } 336 | }, 337 | "pre-commit-hooks-nix": { 338 | "inputs": { 339 | "flake-compat": "flake-compat_2", 340 | "gitignore": "gitignore_2", 341 | "nixpkgs": "nixpkgs_5" 342 | }, 343 | "locked": { 344 | "lastModified": 1747372754, 345 | "narHash": "sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=", 346 | "owner": "cachix", 347 | "repo": "pre-commit-hooks.nix", 348 | "rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46", 349 | "type": "github" 350 | }, 351 | "original": { 352 | "owner": "cachix", 353 | "repo": "pre-commit-hooks.nix", 354 | "type": "github" 355 | } 356 | }, 357 | "root": { 358 | "inputs": { 359 | "devenv": "devenv", 360 | "flake-parts": "flake-parts_2", 361 | "nixpkgs": "nixpkgs_4", 362 | "pre-commit-hooks-nix": "pre-commit-hooks-nix" 363 | } 364 | } 365 | }, 366 | "root": "root", 367 | "version": 7 368 | } 369 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Nix flake for development"; 3 | 4 | inputs = { 5 | nixpkgs = { 6 | url = "github:NixOS/nixpkgs/nixos-unstable"; 7 | }; 8 | 9 | devenv = { 10 | url = "github:cachix/devenv"; 11 | }; 12 | 13 | pre-commit-hooks-nix = { 14 | url = "github:cachix/pre-commit-hooks.nix"; 15 | }; 16 | 17 | flake-parts = { 18 | url = "github:hercules-ci/flake-parts"; 19 | }; 20 | }; 21 | 22 | outputs = inputs@{ flake-parts, ... }: 23 | flake-parts.lib.mkFlake { inherit inputs; } { 24 | imports = [ 25 | inputs.devenv.flakeModule 26 | inputs.pre-commit-hooks-nix.flakeModule 27 | ]; 28 | 29 | systems = [ 30 | "x86_64-linux" 31 | "aarch64-linux" 32 | "x86_64-darwin" 33 | "aarch64-darwin" 34 | ]; 35 | 36 | perSystem = { config, self', inputs', pkgs, system, ... }: { 37 | imports = [ 38 | { 39 | _module.args.pkgs = import inputs.nixpkgs { 40 | inherit system; 41 | config.allowUnfree = true; 42 | }; 43 | } 44 | ]; 45 | 46 | pre-commit = { 47 | settings = { 48 | hooks = { 49 | nixpkgs-fmt = { 50 | enable = true; 51 | }; 52 | golangci-lint = { 53 | enable = true; 54 | }; 55 | }; 56 | }; 57 | }; 58 | 59 | devenv = { 60 | shells = { 61 | default = { 62 | languages = { 63 | go = { 64 | enable = true; 65 | package = pkgs.go_1_23; 66 | }; 67 | }; 68 | 69 | packages = with pkgs; [ 70 | bingo 71 | gnumake 72 | nixpkgs-fmt 73 | ]; 74 | 75 | env = { 76 | CGO_ENABLED = "0"; 77 | 78 | TERRASTATE_LOG_LEVEL = "debug"; 79 | TERRASTATE_LOG_PRETTY = "true"; 80 | TERRASTATE_LOG_COLOR = "true"; 81 | }; 82 | }; 83 | }; 84 | }; 85 | }; 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/webhippie/terrastate 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185 7 | github.com/go-chi/chi/v5 v5.2.1 8 | github.com/joho/godotenv v1.5.1 9 | github.com/oklog/run v1.1.0 10 | github.com/prometheus/client_golang v1.22.0 11 | github.com/rs/zerolog v1.34.0 12 | github.com/spf13/cobra v1.9.1 13 | github.com/spf13/viper v1.20.1 14 | github.com/stretchr/testify v1.10.0 15 | ) 16 | 17 | require ( 18 | github.com/beorn7/perks v1.0.1 // indirect 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 21 | github.com/fsnotify/fsnotify v1.8.0 // indirect 22 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 23 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 24 | github.com/mattn/go-colorable v0.1.13 // indirect 25 | github.com/mattn/go-isatty v0.0.19 // indirect 26 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 27 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 28 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 29 | github.com/prometheus/client_model v0.6.1 // indirect 30 | github.com/prometheus/common v0.62.0 // indirect 31 | github.com/prometheus/procfs v0.15.1 // indirect 32 | github.com/rs/xid v1.6.0 // indirect 33 | github.com/sagikazarmark/locafero v0.7.0 // indirect 34 | github.com/sourcegraph/conc v0.3.0 // indirect 35 | github.com/spf13/afero v1.12.0 // indirect 36 | github.com/spf13/cast v1.7.1 // indirect 37 | github.com/spf13/pflag v1.0.6 // indirect 38 | github.com/subosito/gotenv v1.6.0 // indirect 39 | go.uber.org/atomic v1.9.0 // indirect 40 | go.uber.org/multierr v1.9.0 // indirect 41 | golang.org/x/sys v0.30.0 // indirect 42 | golang.org/x/text v0.21.0 // indirect 43 | google.golang.org/protobuf v1.36.5 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 10 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185 h1:3T8ZyTDp5QxTx3NU48JVb2u+75xc040fofcBaN+6jPA= 12 | github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185/go.mod h1:cFRxtTwTOJkz2x3rQUNCYKWC93yP1VKjR8NUhqFxZNU= 13 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 14 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 15 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 16 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 17 | github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 18 | github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 19 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 20 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 21 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 22 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 23 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 24 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 25 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 26 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 27 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 28 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 29 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 30 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 31 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 32 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 33 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 34 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 35 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 36 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 37 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 38 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 39 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 40 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 42 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 43 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 44 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 45 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 46 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 47 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 48 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 50 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 51 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 52 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 53 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 54 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 55 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 56 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 57 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 58 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 59 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 60 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 61 | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 62 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 63 | github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= 64 | github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= 65 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 66 | github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= 67 | github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= 68 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 69 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 70 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= 71 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= 72 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 73 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 74 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 75 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 76 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 77 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 78 | github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= 79 | github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 80 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 81 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 82 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 83 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 84 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 85 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 86 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 87 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 88 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 89 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 90 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 91 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 92 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 93 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 94 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 95 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 96 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 97 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 98 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 99 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 100 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 101 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 102 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 103 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 104 | -------------------------------------------------------------------------------- /pkg/command/cmd.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | "github.com/webhippie/terrastate/pkg/config" 7 | "github.com/webhippie/terrastate/pkg/version" 8 | ) 9 | 10 | var ( 11 | rootCmd = &cobra.Command{ 12 | Use: "terrastate", 13 | Short: "Terraform HTTP remote state storage", 14 | Version: version.String, 15 | SilenceErrors: false, 16 | SilenceUsage: true, 17 | 18 | PersistentPreRunE: func(_ *cobra.Command, _ []string) error { 19 | return setupLogger() 20 | }, 21 | 22 | CompletionOptions: cobra.CompletionOptions{ 23 | DisableDefaultCmd: true, 24 | }, 25 | } 26 | 27 | cfg *config.Config 28 | ) 29 | 30 | func init() { 31 | cfg = config.Load() 32 | cobra.OnInitialize(setupConfig) 33 | 34 | rootCmd.PersistentFlags().BoolP("help", "h", false, "Show the help, so what you see now") 35 | rootCmd.PersistentFlags().BoolP("version", "v", false, "Print the current version of that tool") 36 | 37 | rootCmd.PersistentFlags().String("config-file", "", "Path to optional config file") 38 | viper.BindPFlag("config.file", rootCmd.PersistentFlags().Lookup("config-file")) 39 | 40 | rootCmd.PersistentFlags().String("log-level", "info", "Set logging level") 41 | viper.SetDefault("log.level", "info") 42 | viper.BindPFlag("log.level", rootCmd.PersistentFlags().Lookup("log-level")) 43 | 44 | rootCmd.PersistentFlags().Bool("log-pretty", true, "Enable pretty logging") 45 | viper.SetDefault("log.pretty", true) 46 | viper.BindPFlag("log.pretty", rootCmd.PersistentFlags().Lookup("log-pretty")) 47 | 48 | rootCmd.PersistentFlags().Bool("log-color", true, "Enable colored logging") 49 | viper.SetDefault("log.color", true) 50 | viper.BindPFlag("log.color", rootCmd.PersistentFlags().Lookup("log-color")) 51 | } 52 | 53 | // Run parses the command line arguments and executes the program. 54 | func Run() error { 55 | return rootCmd.Execute() 56 | } 57 | -------------------------------------------------------------------------------- /pkg/command/cmd_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRun(t *testing.T) { 10 | assert.Nil(t, Run()) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/command/health.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/rs/zerolog/log" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | var ( 14 | healthCmd = &cobra.Command{ 15 | Use: "health", 16 | Short: "Perform health checks", 17 | Run: healthAction, 18 | Args: cobra.NoArgs, 19 | } 20 | ) 21 | 22 | func init() { 23 | rootCmd.AddCommand(healthCmd) 24 | 25 | healthCmd.PersistentFlags().String("metrics-addr", defaultMetricsAddr, "Address to bind the metrics") 26 | viper.SetDefault("metrics.addr", defaultMetricsAddr) 27 | viper.BindPFlag("metrics.addr", healthCmd.PersistentFlags().Lookup("metrics-addr")) 28 | } 29 | 30 | func healthAction(_ *cobra.Command, _ []string) { 31 | resp, err := http.Get( 32 | fmt.Sprintf( 33 | "http://%s/healthz", 34 | cfg.Metrics.Addr, 35 | ), 36 | ) 37 | 38 | if err != nil { 39 | log.Error(). 40 | Err(err). 41 | Msg("failed to request health check") 42 | 43 | os.Exit(1) 44 | } 45 | 46 | defer resp.Body.Close() 47 | 48 | if resp.StatusCode != 200 { 49 | log.Error(). 50 | Int("code", resp.StatusCode). 51 | Msg("health seems to be in bad state") 52 | 53 | os.Exit(1) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/command/server.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "time" 10 | 11 | "github.com/oklog/run" 12 | "github.com/rs/zerolog/log" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | "github.com/webhippie/terrastate/pkg/router" 16 | ) 17 | 18 | var ( 19 | serverCmd = &cobra.Command{ 20 | Use: "server", 21 | Short: "Start integrated server", 22 | Run: serverAction, 23 | Args: cobra.NoArgs, 24 | } 25 | 26 | defaultMetricsAddr = "0.0.0.0:8081" 27 | defaultServerAddr = "0.0.0.0:8080" 28 | defaultServerPprof = false 29 | defaultServerRoot = "/" 30 | defaultServerHost = "http://localhost:8080" 31 | defaultServerCert = "" 32 | defaultServerKey = "" 33 | defaultServerStrictCurves = false 34 | defaultServerStrictCiphers = false 35 | defaultServerStorage = "storage/" 36 | defaultEncryptionSecret = "" 37 | defaultAccessUsername = "" 38 | defaultAccessPassword = "" 39 | ) 40 | 41 | func init() { 42 | rootCmd.AddCommand(serverCmd) 43 | 44 | serverCmd.PersistentFlags().String("metrics-addr", defaultMetricsAddr, "Address to bind the metrics") 45 | viper.SetDefault("metrics.addr", defaultMetricsAddr) 46 | viper.BindPFlag("metrics.addr", serverCmd.PersistentFlags().Lookup("metrics-addr")) 47 | 48 | serverCmd.PersistentFlags().String("metrics-token", "", "Token to make metrics secure") 49 | viper.SetDefault("metrics.token", "") 50 | viper.BindPFlag("metrics.token", serverCmd.PersistentFlags().Lookup("metrics-token")) 51 | 52 | serverCmd.PersistentFlags().String("server-addr", defaultServerAddr, "Address to bind the server") 53 | viper.SetDefault("server.addr", defaultServerAddr) 54 | viper.BindPFlag("server.addr", serverCmd.PersistentFlags().Lookup("server-addr")) 55 | 56 | serverCmd.PersistentFlags().Bool("server-pprof", defaultServerPprof, "Enable pprof debugging") 57 | viper.SetDefault("server.pprof", defaultServerPprof) 58 | viper.BindPFlag("server.pprof", serverCmd.PersistentFlags().Lookup("server-pprof")) 59 | 60 | serverCmd.PersistentFlags().String("server-root", defaultServerRoot, "Root path of the server") 61 | viper.SetDefault("server.root", defaultServerRoot) 62 | viper.BindPFlag("server.root", serverCmd.PersistentFlags().Lookup("server-root")) 63 | 64 | serverCmd.PersistentFlags().String("server-host", defaultServerHost, "External access to server") 65 | viper.SetDefault("server.host", defaultServerHost) 66 | viper.BindPFlag("server.host", serverCmd.PersistentFlags().Lookup("server-host")) 67 | 68 | serverCmd.PersistentFlags().String("server-cert", defaultServerCert, "Path to cert for SSL encryption") 69 | viper.SetDefault("server.cert", defaultServerCert) 70 | viper.BindPFlag("server.cert", serverCmd.PersistentFlags().Lookup("server-cert")) 71 | 72 | serverCmd.PersistentFlags().String("server-key", defaultServerKey, "Path to key for SSL encryption") 73 | viper.SetDefault("server.key", defaultServerKey) 74 | viper.BindPFlag("server.key", serverCmd.PersistentFlags().Lookup("server-key")) 75 | 76 | serverCmd.PersistentFlags().Bool("strict-curves", defaultServerStrictCurves, "Use strict SSL curves") 77 | viper.SetDefault("server.strict_curves", defaultServerStrictCurves) 78 | viper.BindPFlag("server.strict_curves", serverCmd.PersistentFlags().Lookup("strict-curves")) 79 | 80 | serverCmd.PersistentFlags().Bool("strict-ciphers", defaultServerStrictCiphers, "Use strict SSL ciphers") 81 | viper.SetDefault("server.strict_ciphers", defaultServerStrictCiphers) 82 | viper.BindPFlag("server.strict_ciphers", serverCmd.PersistentFlags().Lookup("strict-ciphers")) 83 | 84 | serverCmd.PersistentFlags().String("storage-path", defaultServerStorage, "Folder for storing the states") 85 | viper.SetDefault("server.storage", defaultServerStorage) 86 | viper.BindPFlag("server.storage", serverCmd.PersistentFlags().Lookup("server-storage")) 87 | 88 | serverCmd.PersistentFlags().String("encryption-secret", defaultEncryptionSecret, "Secret for file encryption") 89 | viper.SetDefault("encryption.secret", defaultEncryptionSecret) 90 | viper.BindPFlag("encryption.secret", serverCmd.PersistentFlags().Lookup("encryption-secret")) 91 | 92 | serverCmd.PersistentFlags().String("general-username", defaultAccessUsername, "Username for basic auth") 93 | viper.SetDefault("access.username", defaultAccessUsername) 94 | viper.BindPFlag("access.username", serverCmd.PersistentFlags().Lookup("general-username")) 95 | 96 | serverCmd.PersistentFlags().String("general-password", defaultAccessPassword, "Password for basic auth") 97 | viper.SetDefault("access.password", defaultAccessPassword) 98 | viper.BindPFlag("access.password", serverCmd.PersistentFlags().Lookup("general-password")) 99 | } 100 | 101 | func serverAction(_ *cobra.Command, _ []string) { 102 | var gr run.Group 103 | 104 | if cfg.Server.Cert != "" && cfg.Server.Key != "" { 105 | cert, err := tls.LoadX509KeyPair( 106 | cfg.Server.Cert, 107 | cfg.Server.Key, 108 | ) 109 | 110 | if err != nil { 111 | log.Info(). 112 | Err(err). 113 | Msg("Failed to load certificates") 114 | 115 | os.Exit(1) 116 | } 117 | 118 | server := &http.Server{ 119 | Addr: cfg.Server.Addr, 120 | Handler: router.Load(cfg), 121 | ReadTimeout: 5 * time.Second, 122 | WriteTimeout: 10 * time.Second, 123 | TLSConfig: &tls.Config{ 124 | PreferServerCipherSuites: true, 125 | MinVersion: tls.VersionTLS12, 126 | CurvePreferences: router.Curves(cfg), 127 | CipherSuites: router.Ciphers(cfg), 128 | Certificates: []tls.Certificate{cert}, 129 | }, 130 | } 131 | 132 | gr.Add(func() error { 133 | log.Info(). 134 | Str("addr", cfg.Server.Addr). 135 | Msg("Starting HTTPS server") 136 | 137 | return server.ListenAndServeTLS("", "") 138 | }, func(reason error) { 139 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 140 | defer cancel() 141 | 142 | if err := server.Shutdown(ctx); err != nil { 143 | log.Error(). 144 | Err(err). 145 | Msg("Failed to shutdown HTTPS gracefully") 146 | 147 | return 148 | } 149 | 150 | log.Info(). 151 | Err(reason). 152 | Msg("Shutdown HTTPS gracefully") 153 | }) 154 | } else { 155 | server := &http.Server{ 156 | Addr: cfg.Server.Addr, 157 | Handler: router.Load(cfg), 158 | ReadTimeout: 5 * time.Second, 159 | WriteTimeout: 10 * time.Second, 160 | } 161 | 162 | gr.Add(func() error { 163 | log.Info(). 164 | Str("addr", cfg.Server.Addr). 165 | Msg("Starting HTTP server") 166 | 167 | return server.ListenAndServe() 168 | }, func(reason error) { 169 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 170 | defer cancel() 171 | 172 | if err := server.Shutdown(ctx); err != nil { 173 | log.Error(). 174 | Err(err). 175 | Msg("Failed to shutdown HTTP gracefully") 176 | 177 | return 178 | } 179 | 180 | log.Info(). 181 | Err(reason). 182 | Msg("Shutdown HTTP gracefully") 183 | }) 184 | } 185 | 186 | { 187 | server := &http.Server{ 188 | Addr: cfg.Metrics.Addr, 189 | Handler: router.Metrics(cfg), 190 | ReadTimeout: 5 * time.Second, 191 | WriteTimeout: 10 * time.Second, 192 | } 193 | 194 | gr.Add(func() error { 195 | log.Info(). 196 | Str("addr", cfg.Metrics.Addr). 197 | Msg("Starting metrics server") 198 | 199 | return server.ListenAndServe() 200 | }, func(reason error) { 201 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 202 | defer cancel() 203 | 204 | if err := server.Shutdown(ctx); err != nil { 205 | log.Error(). 206 | Err(err). 207 | Msg("Failed to shutdown metrics gracefully") 208 | 209 | return 210 | } 211 | 212 | log.Info(). 213 | Err(reason). 214 | Msg("Shutdown metrics gracefully") 215 | }) 216 | } 217 | 218 | { 219 | stop := make(chan os.Signal, 1) 220 | 221 | gr.Add(func() error { 222 | signal.Notify(stop, os.Interrupt) 223 | 224 | <-stop 225 | 226 | return nil 227 | }, func(_ error) { 228 | close(stop) 229 | }) 230 | } 231 | 232 | if err := gr.Run(); err != nil { 233 | os.Exit(1) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /pkg/command/setup.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/rs/zerolog" 8 | "github.com/rs/zerolog/log" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | func setupLogger() error { 13 | switch strings.ToLower(viper.GetString("log.level")) { 14 | case "panic": 15 | zerolog.SetGlobalLevel(zerolog.PanicLevel) 16 | case "fatal": 17 | zerolog.SetGlobalLevel(zerolog.FatalLevel) 18 | case "error": 19 | zerolog.SetGlobalLevel(zerolog.ErrorLevel) 20 | case "warn": 21 | zerolog.SetGlobalLevel(zerolog.WarnLevel) 22 | case "info": 23 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 24 | case "debug": 25 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 26 | default: 27 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 28 | } 29 | 30 | if viper.GetBool("log.pretty") { 31 | log.Logger = log.Output( 32 | zerolog.ConsoleWriter{ 33 | Out: os.Stderr, 34 | NoColor: !viper.GetBool("log.color"), 35 | }, 36 | ) 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func setupConfig() { 43 | if viper.GetString("config.file") != "" { 44 | viper.SetConfigFile(viper.GetString("config.file")) 45 | } else { 46 | viper.SetConfigName("config") 47 | viper.AddConfigPath("/etc/terrastate") 48 | viper.AddConfigPath("$HOME/.terrastate") 49 | viper.AddConfigPath("./terrastate") 50 | } 51 | 52 | viper.SetEnvPrefix("terrastate") 53 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 54 | viper.AutomaticEnv() 55 | 56 | if err := readConfig(); err != nil { 57 | log.Error(). 58 | Err(err). 59 | Msg("Failed to read config file") 60 | } 61 | 62 | if err := viper.Unmarshal(cfg); err != nil { 63 | log.Error(). 64 | Err(err). 65 | Msg("Failed to parse config file") 66 | } 67 | } 68 | 69 | func readConfig() error { 70 | err := viper.ReadInConfig() 71 | 72 | if err == nil { 73 | return nil 74 | } 75 | 76 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 77 | return nil 78 | } 79 | 80 | if _, ok := err.(*os.PathError); ok { 81 | return nil 82 | } 83 | 84 | return err 85 | } 86 | -------------------------------------------------------------------------------- /pkg/command/state.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/dchest/safefile" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | "github.com/webhippie/terrastate/pkg/helper" 14 | ) 15 | 16 | var ( 17 | stateCmd = &cobra.Command{ 18 | Use: "state", 19 | Short: "Read and update state", 20 | } 21 | 22 | stateListCmd = &cobra.Command{ 23 | Use: "list", 24 | Short: "List all states", 25 | Run: stateListAction, 26 | Args: cobra.NoArgs, 27 | } 28 | 29 | stateShowCmd = &cobra.Command{ 30 | Use: "show ", 31 | Short: "Show a state", 32 | Run: stateShowAction, 33 | Args: func(_ *cobra.Command, args []string) error { 34 | if len(args) != 1 { 35 | return fmt.Errorf("missing state argument") 36 | } 37 | 38 | return nil 39 | }, 40 | } 41 | 42 | stateEncryptCmd = &cobra.Command{ 43 | Use: "encrypt ", 44 | Short: "Encrypt a state", 45 | Run: stateEncryptAction, 46 | Args: func(_ *cobra.Command, args []string) error { 47 | if len(args) != 1 { 48 | return fmt.Errorf("missing state argument") 49 | } 50 | 51 | return nil 52 | }, 53 | } 54 | 55 | stateDecryptCmd = &cobra.Command{ 56 | Use: "decrypt ", 57 | Short: "Decrypt a state", 58 | Run: stateDecryptAction, 59 | Args: func(_ *cobra.Command, args []string) error { 60 | if len(args) != 1 { 61 | return fmt.Errorf("missing state argument") 62 | } 63 | 64 | return nil 65 | }, 66 | } 67 | ) 68 | 69 | func init() { 70 | rootCmd.AddCommand(stateCmd) 71 | 72 | stateCmd.PersistentFlags().String("storage-path", defaultServerStorage, "Folder for storing the states") 73 | viper.SetDefault("server.storage", defaultServerStorage) 74 | viper.BindPFlag("server.storage", stateCmd.PersistentFlags().Lookup("server-storage")) 75 | 76 | stateCmd.PersistentFlags().String("encryption-secret", defaultEncryptionSecret, "Secret for file encryption") 77 | viper.SetDefault("encryption.secret", defaultEncryptionSecret) 78 | viper.BindPFlag("encryption.secret", stateCmd.PersistentFlags().Lookup("encryption-secret")) 79 | 80 | stateCmd.AddCommand(stateListCmd) 81 | stateCmd.AddCommand(stateShowCmd) 82 | stateCmd.AddCommand(stateEncryptCmd) 83 | stateCmd.AddCommand(stateDecryptCmd) 84 | } 85 | 86 | func stateListAction(_ *cobra.Command, _ []string) { 87 | var ( 88 | states []string 89 | ) 90 | 91 | err := filepath.Walk(cfg.Server.Storage, func(path string, info os.FileInfo, err error) error { 92 | if err != nil { 93 | return err 94 | } 95 | 96 | if info.Name() == "terraform.tfstate" { 97 | state := strings.TrimPrefix( 98 | strings.Replace( 99 | filepath.Dir( 100 | path, 101 | ), 102 | cfg.Server.Storage, 103 | "", 104 | -1, 105 | ), 106 | "/", 107 | ) 108 | 109 | states = append(states, state) 110 | } 111 | 112 | return nil 113 | }) 114 | 115 | if err != nil { 116 | cobra.CheckErr("Failed to list") 117 | } 118 | 119 | if len(states) > 0 { 120 | fmt.Fprintln(os.Stdout, strings.Join(states, "\n")) 121 | } 122 | } 123 | 124 | func stateShowAction(_ *cobra.Command, args []string) { 125 | state := args[0] 126 | 127 | full := path.Join( 128 | cfg.Server.Storage, 129 | state, 130 | "terraform.tfstate", 131 | ) 132 | 133 | if _, err := os.Stat(full); os.IsNotExist(err) { 134 | cobra.CheckErr("State does not exist") 135 | } 136 | 137 | file, err := os.ReadFile( 138 | full, 139 | ) 140 | 141 | if err != nil { 142 | cobra.CheckErr("Failed to read state") 143 | } 144 | 145 | fmt.Fprintln(os.Stdout, string(file)) 146 | } 147 | 148 | func stateEncryptAction(_ *cobra.Command, args []string) { 149 | if cfg.Encryption.Secret == "" { 150 | cobra.CheckErr("Missing encryption secret") 151 | } 152 | 153 | state := args[0] 154 | 155 | full := path.Join( 156 | cfg.Server.Storage, 157 | state, 158 | "terraform.tfstate", 159 | ) 160 | 161 | if _, err := os.Stat(full); os.IsNotExist(err) { 162 | cobra.CheckErr("State does not exist") 163 | } 164 | 165 | file, err := os.ReadFile( 166 | full, 167 | ) 168 | 169 | if err != nil { 170 | cobra.CheckErr("Failed to read state") 171 | } 172 | 173 | encrypted, err := helper.Encrypt( 174 | file, 175 | []byte(cfg.Encryption.Secret), 176 | ) 177 | 178 | if err != nil { 179 | cobra.CheckErr("Failed to encrypt state") 180 | } 181 | 182 | if err := safefile.WriteFile(full, encrypted, 0644); err != nil { 183 | cobra.CheckErr("Failed to update file") 184 | } 185 | 186 | fmt.Fprintln(os.Stderr, "Successfully encrypted state") 187 | } 188 | 189 | func stateDecryptAction(_ *cobra.Command, args []string) { 190 | if cfg.Encryption.Secret == "" { 191 | cobra.CheckErr("Missing encryption secret") 192 | } 193 | 194 | state := args[0] 195 | 196 | full := path.Join( 197 | cfg.Server.Storage, 198 | state, 199 | "terraform.tfstate", 200 | ) 201 | 202 | if _, err := os.Stat(full); os.IsNotExist(err) { 203 | cobra.CheckErr("State does not exist") 204 | } 205 | 206 | file, err := os.ReadFile( 207 | full, 208 | ) 209 | 210 | if err != nil { 211 | cobra.CheckErr("Failed to read state") 212 | } 213 | 214 | decrypted, err := helper.Decrypt( 215 | file, 216 | []byte(cfg.Encryption.Secret), 217 | ) 218 | 219 | if err != nil { 220 | cobra.CheckErr("Failed to decrypt state") 221 | } 222 | 223 | if err := safefile.WriteFile(full, decrypted, 0644); err != nil { 224 | cobra.CheckErr("Failed to update file") 225 | } 226 | 227 | fmt.Fprintln(os.Stderr, "Successfully encrypted state") 228 | } 229 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Server defines the server configuration. 4 | type Server struct { 5 | Addr string `mapstructure:"addr"` 6 | Host string `mapstructure:"host"` 7 | Pprof bool `mapstructure:"pprof"` 8 | Root string `mapstructure:"root"` 9 | Cert string `mapstructure:"cert"` 10 | Key string `mapstructure:"key"` 11 | StrictCurves bool `mapstructure:"strict_curves"` 12 | StrictCiphers bool `mapstructure:"strict_ciphers"` 13 | Storage string `mapstructure:"storage"` 14 | } 15 | 16 | // Metrics defines the metrics server configuration. 17 | type Metrics struct { 18 | Addr string `mapstructure:"addr"` 19 | Token string `mapstructure:"token"` 20 | } 21 | 22 | // Logs defines the level and color for log configuration. 23 | type Logs struct { 24 | Level string `mapstructure:"level"` 25 | Pretty bool `mapstructure:"pretty"` 26 | Color bool `mapstructure:"color"` 27 | } 28 | 29 | // Encryption defines the encryption configuration. 30 | type Encryption struct { 31 | Secret string `mapstructure:"secret"` 32 | } 33 | 34 | // Access defines the access configuration. 35 | type Access struct { 36 | Username string `mapstructure:"username"` 37 | Password string `mapstructure:"password"` 38 | } 39 | 40 | // Config defines the general configuration. 41 | type Config struct { 42 | Server Server `mapstructure:"server"` 43 | Metrics Metrics `mapstructure:"metrics"` 44 | Logs Logs `mapstructure:"log"` 45 | Encryption Encryption `mapstructure:"encryption"` 46 | Access Access `mapstructure:"access"` 47 | } 48 | 49 | // Load initializes a default configuration struct. 50 | func Load() *Config { 51 | return &Config{} 52 | } 53 | -------------------------------------------------------------------------------- /pkg/handler/delete.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "path" 7 | "strings" 8 | "time" 9 | 10 | "github.com/go-chi/chi/v5" 11 | "github.com/rs/zerolog/log" 12 | "github.com/webhippie/terrastate/pkg/config" 13 | ) 14 | 15 | // Delete is used to purge a specific state. 16 | func Delete(cfg *config.Config) http.HandlerFunc { 17 | return func(w http.ResponseWriter, req *http.Request) { 18 | defer handleMetrics(time.Now(), "delete", chi.URLParam(req, "*")) 19 | 20 | dir := strings.Replace( 21 | path.Join( 22 | cfg.Server.Storage, 23 | chi.URLParam(req, "*"), 24 | ), 25 | "../", "", -1, 26 | ) 27 | 28 | full := path.Join( 29 | dir, 30 | "terraform.tfstate", 31 | ) 32 | 33 | if _, err := os.Stat(full); os.IsNotExist(err) { 34 | log.Error(). 35 | Str("file", full). 36 | Msg("State file does not exist") 37 | 38 | http.Error( 39 | w, 40 | http.StatusText(http.StatusNotFound), 41 | http.StatusNotFound, 42 | ) 43 | 44 | return 45 | } 46 | 47 | if err := os.Remove(full); err != nil { 48 | log.Error(). 49 | Err(err). 50 | Str("file", full). 51 | Msg("Failed to delete state file") 52 | 53 | http.Error( 54 | w, 55 | http.StatusText(http.StatusInternalServerError), 56 | http.StatusInternalServerError, 57 | ) 58 | 59 | return 60 | } 61 | 62 | log.Info(). 63 | Str("file", full). 64 | Msg("Successfully deleted state file") 65 | 66 | w.Header().Set("Content-Type", "application/json") 67 | w.WriteHeader(http.StatusOK) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/handler/fetch.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "path" 8 | "strings" 9 | "time" 10 | 11 | "github.com/go-chi/chi/v5" 12 | "github.com/rs/zerolog/log" 13 | "github.com/webhippie/terrastate/pkg/config" 14 | "github.com/webhippie/terrastate/pkg/helper" 15 | ) 16 | 17 | // Fetch is used to fetch a specific state. 18 | func Fetch(cfg *config.Config) http.HandlerFunc { 19 | return func(w http.ResponseWriter, req *http.Request) { 20 | defer handleMetrics(time.Now(), "fetch", chi.URLParam(req, "*")) 21 | 22 | dir := strings.Replace( 23 | path.Join( 24 | cfg.Server.Storage, 25 | chi.URLParam(req, "*"), 26 | ), 27 | "../", "", -1, 28 | ) 29 | 30 | full := path.Join( 31 | dir, 32 | "terraform.tfstate", 33 | ) 34 | 35 | if _, err := os.Stat(full); os.IsNotExist(err) { 36 | log.Error(). 37 | Str("file", full). 38 | Msg("State file does not exist") 39 | 40 | http.Error( 41 | w, 42 | http.StatusText(http.StatusNotFound), 43 | http.StatusNotFound, 44 | ) 45 | 46 | return 47 | } 48 | 49 | file, err := os.ReadFile( 50 | full, 51 | ) 52 | 53 | if err != nil { 54 | log.Error(). 55 | Err(err). 56 | Str("file", full). 57 | Msg("Failed to read state file") 58 | 59 | http.Error( 60 | w, 61 | http.StatusText(http.StatusInternalServerError), 62 | http.StatusInternalServerError, 63 | ) 64 | 65 | return 66 | } 67 | 68 | if cfg.Encryption.Secret != "" { 69 | decrypted, err := helper.Decrypt(file, []byte(cfg.Encryption.Secret)) 70 | 71 | if err != nil { 72 | log.Info(). 73 | Err(err). 74 | Str("file", full). 75 | Msg("Failed to decrypt the state") 76 | 77 | http.Error( 78 | w, 79 | http.StatusText(http.StatusInternalServerError), 80 | http.StatusInternalServerError, 81 | ) 82 | 83 | return 84 | } 85 | 86 | file = decrypted 87 | } 88 | 89 | w.Header().Set("Content-Type", "application/json") 90 | w.WriteHeader(http.StatusOK) 91 | 92 | fmt.Fprintln(w, string(file)) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pkg/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/webhippie/terrastate/pkg/config" 8 | ) 9 | 10 | // Notfound just returns a 404 not found error. 11 | func Notfound(_ *config.Config) http.HandlerFunc { 12 | return func(w http.ResponseWriter, _ *http.Request) { 13 | defer handleMetrics(time.Now(), "notfound", "") 14 | 15 | http.Error( 16 | w, 17 | http.StatusText(http.StatusNotFound), 18 | http.StatusNotFound, 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/handler/lock.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "os" 7 | "path" 8 | "strings" 9 | "time" 10 | 11 | "github.com/dchest/safefile" 12 | "github.com/go-chi/chi/v5" 13 | "github.com/rs/zerolog/log" 14 | "github.com/webhippie/terrastate/pkg/config" 15 | "github.com/webhippie/terrastate/pkg/model" 16 | ) 17 | 18 | // Lock is used to lock a specific state. 19 | func Lock(cfg *config.Config) http.HandlerFunc { 20 | return func(w http.ResponseWriter, req *http.Request) { 21 | defer handleMetrics(time.Now(), "lock", chi.URLParam(req, "*")) 22 | 23 | dir := strings.Replace( 24 | path.Join( 25 | cfg.Server.Storage, 26 | chi.URLParam(req, "*"), 27 | ), 28 | "../", "", -1, 29 | ) 30 | 31 | full := path.Join( 32 | dir, 33 | "terraform.lock", 34 | ) 35 | 36 | requested := model.LockInfo{} 37 | 38 | if err := json.NewDecoder(req.Body).Decode(&requested); err != nil { 39 | log.Error(). 40 | Err(err). 41 | Msg("Failed to parse body") 42 | 43 | http.Error( 44 | w, 45 | http.StatusText(http.StatusInternalServerError), 46 | http.StatusInternalServerError, 47 | ) 48 | 49 | return 50 | } 51 | 52 | if _, err := os.Stat(full); !os.IsNotExist(err) { 53 | existing := model.LockInfo{} 54 | 55 | file, err := os.ReadFile( 56 | full, 57 | ) 58 | 59 | if err != nil { 60 | log.Error(). 61 | Err(err). 62 | Str("file", full). 63 | Msg("Failed to read lock file") 64 | 65 | http.Error( 66 | w, 67 | http.StatusText(http.StatusInternalServerError), 68 | http.StatusInternalServerError, 69 | ) 70 | 71 | return 72 | } 73 | 74 | if err := json.Unmarshal(file, &existing); err != nil { 75 | log.Error(). 76 | Err(err). 77 | Str("file", full). 78 | Msg("Failed to parse lock file") 79 | 80 | http.Error( 81 | w, 82 | http.StatusText(http.StatusInternalServerError), 83 | http.StatusInternalServerError, 84 | ) 85 | 86 | return 87 | } 88 | 89 | log.Info(). 90 | Str("existing", existing.ID). 91 | Str("requested", requested.ID). 92 | Msg("Lock file already exists") 93 | 94 | w.Header().Set("Content-Type", "application/json") 95 | w.WriteHeader(http.StatusLocked) 96 | 97 | json.NewEncoder(w).Encode(existing) 98 | return 99 | } 100 | 101 | if err := os.MkdirAll(dir, 0755); err != nil { 102 | log.Error(). 103 | Err(err). 104 | Str("dir", dir). 105 | Msg("Failed to create lock dir") 106 | 107 | http.Error( 108 | w, 109 | http.StatusText(http.StatusInternalServerError), 110 | http.StatusInternalServerError, 111 | ) 112 | 113 | return 114 | } 115 | 116 | marshaled, _ := json.Marshal(requested) 117 | 118 | if err := safefile.WriteFile(full, marshaled, 0644); err != nil { 119 | log.Error(). 120 | Err(err). 121 | Str("file", full). 122 | Msg("Failed to write lock file") 123 | 124 | http.Error( 125 | w, 126 | http.StatusText(http.StatusInternalServerError), 127 | http.StatusInternalServerError, 128 | ) 129 | 130 | return 131 | } 132 | 133 | w.Header().Set("Content-Type", "application/json") 134 | w.WriteHeader(http.StatusOK) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /pkg/handler/metrics.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/prometheus/client_golang/prometheus/collectors" 8 | "github.com/webhippie/terrastate/pkg/version" 9 | ) 10 | 11 | const ( 12 | namespace = "terrastate" 13 | ) 14 | 15 | var ( 16 | requestCounter = prometheus.NewCounterVec( 17 | prometheus.CounterOpts{ 18 | Namespace: namespace, 19 | Subsystem: "http", 20 | Name: "request_count_total", 21 | Help: "counter of http requests made", 22 | }, 23 | []string{"action", "state"}, 24 | ) 25 | 26 | requestDuration = prometheus.NewHistogramVec( 27 | prometheus.HistogramOpts{ 28 | Namespace: namespace, 29 | Subsystem: "http", 30 | Name: "request_duration_milliseconds", 31 | Help: "histogram of the time (in milliseconds) each request took", 32 | Buckets: append([]float64{.001, .003}, prometheus.DefBuckets...), 33 | }, 34 | []string{"action", "state"}, 35 | ) 36 | ) 37 | 38 | func init() { 39 | prometheus.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{ 40 | Namespace: namespace, 41 | })) 42 | 43 | prometheus.MustRegister(version.Collector(namespace)) 44 | 45 | prometheus.MustRegister(requestCounter) 46 | prometheus.MustRegister(requestDuration) 47 | } 48 | 49 | func handleMetrics(start time.Time, action, state string) { 50 | duration := time.Since(start).Seconds() * 1e3 51 | 52 | requestCounter.WithLabelValues(action, state).Inc() 53 | requestDuration.WithLabelValues(action, state).Observe(duration) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/handler/unlock.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "os" 7 | "path" 8 | "strings" 9 | "time" 10 | 11 | "github.com/go-chi/chi/v5" 12 | "github.com/rs/zerolog/log" 13 | "github.com/webhippie/terrastate/pkg/config" 14 | "github.com/webhippie/terrastate/pkg/model" 15 | ) 16 | 17 | // Unlock is used to unlock a specific state. 18 | func Unlock(cfg *config.Config) http.HandlerFunc { 19 | return func(w http.ResponseWriter, req *http.Request) { 20 | defer handleMetrics(time.Now(), "unlock", chi.URLParam(req, "*")) 21 | 22 | dir := strings.Replace( 23 | path.Join( 24 | cfg.Server.Storage, 25 | chi.URLParam(req, "*"), 26 | ), 27 | "../", "", -1, 28 | ) 29 | 30 | full := path.Join( 31 | dir, 32 | "terraform.lock", 33 | ) 34 | 35 | requested := model.LockInfo{} 36 | 37 | if err := json.NewDecoder(req.Body).Decode(&requested); err != nil { 38 | log.Error(). 39 | Err(err). 40 | Msg("Failed to parse body") 41 | 42 | http.Error( 43 | w, 44 | http.StatusText(http.StatusInternalServerError), 45 | http.StatusInternalServerError, 46 | ) 47 | 48 | return 49 | } 50 | 51 | existing := model.LockInfo{} 52 | 53 | file, err := os.ReadFile( 54 | full, 55 | ) 56 | 57 | if err != nil { 58 | log.Error(). 59 | Err(err). 60 | Str("file", full). 61 | Msg("Failed to read lock file") 62 | 63 | http.Error( 64 | w, 65 | http.StatusText(http.StatusInternalServerError), 66 | http.StatusInternalServerError, 67 | ) 68 | 69 | return 70 | } 71 | 72 | if err := json.Unmarshal(file, &existing); err != nil { 73 | log.Error(). 74 | Err(err). 75 | Str("file", full). 76 | Msg("Failed to parse lock file") 77 | 78 | http.Error( 79 | w, 80 | http.StatusText(http.StatusInternalServerError), 81 | http.StatusInternalServerError, 82 | ) 83 | 84 | return 85 | } 86 | 87 | if err := os.Remove(full); err != nil { 88 | log.Error(). 89 | Err(err). 90 | Str("file", full). 91 | Msg("Failed to delete lock file") 92 | 93 | http.Error( 94 | w, 95 | http.StatusText(http.StatusInternalServerError), 96 | http.StatusInternalServerError, 97 | ) 98 | 99 | return 100 | } 101 | 102 | log.Info(). 103 | Str("existing", existing.ID). 104 | Str("requested", requested.ID). 105 | Msg("Successfully unlocked state") 106 | 107 | w.Header().Set("Content-Type", "application/json") 108 | w.WriteHeader(http.StatusOK) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pkg/handler/update.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | "path" 8 | "strings" 9 | "time" 10 | 11 | "github.com/dchest/safefile" 12 | "github.com/go-chi/chi/v5" 13 | "github.com/rs/zerolog/log" 14 | "github.com/webhippie/terrastate/pkg/config" 15 | "github.com/webhippie/terrastate/pkg/helper" 16 | ) 17 | 18 | // Update is used to update a specific state. 19 | func Update(cfg *config.Config) http.HandlerFunc { 20 | return func(w http.ResponseWriter, req *http.Request) { 21 | defer handleMetrics(time.Now(), "update", chi.URLParam(req, "*")) 22 | 23 | dir := strings.Replace( 24 | path.Join( 25 | cfg.Server.Storage, 26 | chi.URLParam(req, "*"), 27 | ), 28 | "../", "", -1, 29 | ) 30 | 31 | full := path.Join( 32 | dir, 33 | "terraform.tfstate", 34 | ) 35 | 36 | content, err := io.ReadAll(req.Body) 37 | 38 | if err != nil { 39 | log.Error(). 40 | Err(err). 41 | Msg("Failed to load request body") 42 | 43 | http.Error( 44 | w, 45 | http.StatusText(http.StatusInternalServerError), 46 | http.StatusInternalServerError, 47 | ) 48 | 49 | return 50 | } 51 | 52 | if err := os.MkdirAll(dir, 0755); err != nil { 53 | log.Error(). 54 | Err(err). 55 | Str("dir", dir). 56 | Msg("Failed to create state dir") 57 | 58 | http.Error( 59 | w, 60 | http.StatusText(http.StatusInternalServerError), 61 | http.StatusInternalServerError, 62 | ) 63 | 64 | return 65 | } 66 | 67 | if cfg.Encryption.Secret != "" { 68 | encrypted, err := helper.Encrypt(content, []byte(cfg.Encryption.Secret)) 69 | 70 | if err != nil { 71 | log.Error(). 72 | Err(err). 73 | Str("file", full). 74 | Msg("Failed to encrypt the state") 75 | 76 | http.Error( 77 | w, 78 | http.StatusText(http.StatusInternalServerError), 79 | http.StatusInternalServerError, 80 | ) 81 | 82 | return 83 | } 84 | 85 | content = encrypted 86 | } 87 | 88 | if _, err := os.Stat(full); os.IsNotExist(err) { 89 | if err := safefile.WriteFile(full, content, 0644); err != nil { 90 | log.Error(). 91 | Err(err). 92 | Str("file", full). 93 | Msg("Failed to create state file") 94 | 95 | http.Error( 96 | w, 97 | http.StatusText(http.StatusInternalServerError), 98 | http.StatusInternalServerError, 99 | ) 100 | 101 | return 102 | } 103 | 104 | log.Info(). 105 | Str("file", full). 106 | Msg("Successfully created state file") 107 | } else { 108 | if err := safefile.WriteFile(full, content, 0644); err != nil { 109 | log.Error(). 110 | Err(err). 111 | Str("file", full). 112 | Msg("Failed to update state file") 113 | 114 | http.Error( 115 | w, 116 | http.StatusText(http.StatusInternalServerError), 117 | http.StatusInternalServerError, 118 | ) 119 | 120 | return 121 | } 122 | 123 | log.Info(). 124 | Str("file", full). 125 | Msg("Successfully updated state file") 126 | } 127 | 128 | w.Header().Set("Content-Type", "application/json") 129 | w.WriteHeader(http.StatusOK) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /pkg/helper/encryption.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "encoding/base64" 8 | "fmt" 9 | "io" 10 | ) 11 | 12 | // Encrypt encrypts a given payload by given key. 13 | func Encrypt(plaintext []byte, key []byte) ([]byte, error) { 14 | c, err := aes.NewCipher(key) 15 | 16 | if err != nil { 17 | return nil, fmt.Errorf("failed to create cipher: %w", err) 18 | } 19 | 20 | gcm, err := cipher.NewGCM(c) 21 | 22 | if err != nil { 23 | return nil, fmt.Errorf("failed to create gcm: %w", err) 24 | } 25 | 26 | nonce := make([]byte, gcm.NonceSize()) 27 | 28 | if _, err = io.ReadFull(rand.Reader, nonce); err != nil { 29 | return nil, fmt.Errorf("failed to create nonce: %w", err) 30 | } 31 | 32 | ciphertext, err := gcm.Seal(nonce, nonce, plaintext, nil), nil 33 | 34 | if err != nil { 35 | return nil, fmt.Errorf("failed to seal content: %w", err) 36 | } 37 | 38 | return []byte(base64.StdEncoding.EncodeToString(ciphertext)), nil 39 | } 40 | 41 | // Decrypt decrypts a given payload by given key. 42 | func Decrypt(ciphertext []byte, key []byte) ([]byte, error) { 43 | decoded, err := base64.StdEncoding.DecodeString(string(ciphertext)) 44 | 45 | if err != nil { 46 | return decoded, nil 47 | } 48 | 49 | c, err := aes.NewCipher(key) 50 | 51 | if err != nil { 52 | return nil, fmt.Errorf("failed to create cipher: %w", err) 53 | } 54 | 55 | gcm, err := cipher.NewGCM(c) 56 | 57 | if err != nil { 58 | return nil, fmt.Errorf("failed to create gcm: %w", err) 59 | } 60 | 61 | nonceSize := gcm.NonceSize() 62 | 63 | if len(decoded) < nonceSize { 64 | return nil, fmt.Errorf("ciphertext too short") 65 | } 66 | 67 | nonce, ciphertext := decoded[:nonceSize], decoded[nonceSize:] 68 | return gcm.Open(nil, nonce, ciphertext, nil) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/middleware/basicauth/basicauth.go: -------------------------------------------------------------------------------- 1 | package basicauth 2 | 3 | import ( 4 | "encoding/base64" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/webhippie/terrastate/pkg/config" 9 | ) 10 | 11 | // Basicauth integrates a simple basic authentication. 12 | func Basicauth(cfg *config.Config) func(next http.Handler) http.Handler { 13 | return func(next http.Handler) http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | if cfg.Access.Username != "" && cfg.Access.Password != "" { 16 | w.Header().Set("WWW-Authenticate", `Basic realm="Terrastate"`) 17 | 18 | s := strings.SplitN(r.Header.Get("Authorization"), " ", 2) 19 | 20 | if len(s) != 2 { 21 | http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 22 | return 23 | } 24 | 25 | b, err := base64.StdEncoding.DecodeString(s[1]) 26 | 27 | if err != nil { 28 | http.Error(w, err.Error(), http.StatusUnauthorized) 29 | return 30 | } 31 | 32 | pair := strings.SplitN(string(b), ":", 2) 33 | 34 | if len(pair) != 2 { 35 | http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 36 | return 37 | } 38 | 39 | if pair[0] != cfg.Access.Username { 40 | http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 41 | return 42 | } 43 | 44 | if pair[1] != cfg.Access.Password { 45 | http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 46 | return 47 | } 48 | } 49 | 50 | next.ServeHTTP(w, r) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/middleware/header/header.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/webhippie/terrastate/pkg/version" 8 | ) 9 | 10 | // Cache writes required cache headers to all requests. 11 | func Cache(next http.Handler) http.Handler { 12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | w.Header().Set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value") 14 | w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") 15 | w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) 16 | 17 | next.ServeHTTP(w, r) 18 | }) 19 | } 20 | 21 | // Options writes required option headers to all requests. 22 | func Options(next http.Handler) http.Handler { 23 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 | if r.Method != "OPTIONS" { 25 | next.ServeHTTP(w, r) 26 | } else { 27 | w.Header().Set("Access-Control-Allow-Origin", "*") 28 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") 29 | w.Header().Set("Access-Control-Allow-Headers", "authorization, origin, content-type, accept") 30 | w.Header().Set("Allow", "HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS") 31 | 32 | w.WriteHeader(http.StatusOK) 33 | } 34 | }) 35 | } 36 | 37 | // Secure writes required access headers to all requests. 38 | func Secure(next http.Handler) http.Handler { 39 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 40 | w.Header().Set("Access-Control-Allow-Origin", "*") 41 | w.Header().Set("X-Frame-Options", "DENY") 42 | w.Header().Set("X-Content-Type-Options", "nosniff") 43 | w.Header().Set("X-XSS-Protection", "1; mode=block") 44 | 45 | if r.TLS != nil { 46 | w.Header().Set("Strict-Transport-Security", "max-age=31536000") 47 | } 48 | 49 | next.ServeHTTP(w, r) 50 | }) 51 | } 52 | 53 | // Version writes the current API version to the headers. 54 | func Version(next http.Handler) http.Handler { 55 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 | w.Header().Set("X-TERRASTATE-VERSION", version.String) 57 | 58 | next.ServeHTTP(w, r) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/middleware/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | ) 8 | 9 | // Handler initializes the prometheus middleware. 10 | func Handler(token string) http.HandlerFunc { 11 | h := promhttp.Handler() 12 | 13 | return func(w http.ResponseWriter, r *http.Request) { 14 | if token == "" { 15 | h.ServeHTTP(w, r) 16 | return 17 | } 18 | 19 | header := r.Header.Get("Authorization") 20 | 21 | if header == "" { 22 | http.Error(w, "Invalid or missing token", http.StatusUnauthorized) 23 | return 24 | } 25 | 26 | if header != "Bearer "+token { 27 | http.Error(w, "Invalid or missing token", http.StatusUnauthorized) 28 | return 29 | } 30 | 31 | h.ServeHTTP(w, r) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/model/lock_info.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // LockInfo gets sent by Terraform as locking payload. 4 | type LockInfo struct { 5 | ID string 6 | Operation string 7 | Info string 8 | Who string 9 | Version string 10 | Created string 11 | Path string 12 | } 13 | -------------------------------------------------------------------------------- /pkg/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "crypto/tls" 5 | "io" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/go-chi/chi/v5" 10 | "github.com/go-chi/chi/v5/middleware" 11 | "github.com/rs/zerolog/hlog" 12 | "github.com/rs/zerolog/log" 13 | "github.com/webhippie/terrastate/pkg/config" 14 | "github.com/webhippie/terrastate/pkg/handler" 15 | "github.com/webhippie/terrastate/pkg/middleware/basicauth" 16 | "github.com/webhippie/terrastate/pkg/middleware/header" 17 | "github.com/webhippie/terrastate/pkg/middleware/prometheus" 18 | ) 19 | 20 | // Load initializes the routing of the application. 21 | func Load(cfg *config.Config) http.Handler { 22 | mux := chi.NewRouter() 23 | 24 | mux.Use(hlog.NewHandler(log.Logger)) 25 | mux.Use(hlog.RemoteAddrHandler("ip")) 26 | mux.Use(hlog.URLHandler("path")) 27 | mux.Use(hlog.MethodHandler("method")) 28 | mux.Use(hlog.RequestIDHandler("request_id", "Request-Id")) 29 | 30 | mux.Use(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { 31 | hlog.FromRequest(r).Debug(). 32 | Str("method", r.Method). 33 | Str("url", r.URL.String()). 34 | Int("status", status). 35 | Int("size", size). 36 | Dur("duration", duration). 37 | Msg("") 38 | })) 39 | 40 | mux.Use(middleware.Timeout(60 * time.Second)) 41 | mux.Use(middleware.RealIP) 42 | mux.Use(header.Version) 43 | mux.Use(header.Cache) 44 | mux.Use(header.Secure) 45 | mux.Use(header.Options) 46 | 47 | mux.NotFound(handler.Notfound(cfg)) 48 | 49 | mux.Route(cfg.Server.Root, func(root chi.Router) { 50 | if cfg.Server.Pprof { 51 | root.Mount("/debug", middleware.Profiler()) 52 | } 53 | 54 | root.Route("/remote", func(state chi.Router) { 55 | state.Use(basicauth.Basicauth(cfg)) 56 | 57 | state.Method("get", "/*", handler.Fetch(cfg)) 58 | state.Method("post", "/*", handler.Update(cfg)) 59 | state.Method("delete", "/*", handler.Delete(cfg)) 60 | state.Method("lock", "/*", handler.Lock(cfg)) 61 | state.Method("unlock", "/*", handler.Unlock(cfg)) 62 | }) 63 | }) 64 | 65 | return mux 66 | } 67 | 68 | // Metrics initializes the routing of metrics and health. 69 | func Metrics(cfg *config.Config) http.Handler { 70 | mux := chi.NewRouter() 71 | 72 | mux.Use(hlog.NewHandler(log.Logger)) 73 | mux.Use(hlog.RemoteAddrHandler("ip")) 74 | mux.Use(hlog.URLHandler("path")) 75 | mux.Use(hlog.MethodHandler("method")) 76 | mux.Use(hlog.RequestIDHandler("request_id", "Request-Id")) 77 | 78 | mux.Use(middleware.Timeout(60 * time.Second)) 79 | mux.Use(middleware.RealIP) 80 | mux.Use(header.Version) 81 | mux.Use(header.Cache) 82 | mux.Use(header.Secure) 83 | mux.Use(header.Options) 84 | 85 | mux.Route("/", func(root chi.Router) { 86 | root.Get("/metrics", prometheus.Handler(cfg.Metrics.Token)) 87 | 88 | root.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { 89 | w.Header().Set("Content-Type", "text/plain") 90 | w.WriteHeader(http.StatusOK) 91 | 92 | io.WriteString(w, http.StatusText(http.StatusOK)) 93 | }) 94 | 95 | root.Get("/readyz", func(w http.ResponseWriter, _ *http.Request) { 96 | w.Header().Set("Content-Type", "text/plain") 97 | w.WriteHeader(http.StatusOK) 98 | 99 | io.WriteString(w, http.StatusText(http.StatusOK)) 100 | }) 101 | }) 102 | 103 | return mux 104 | } 105 | 106 | // Curves provides optionally a list of secure curves. 107 | func Curves(cfg *config.Config) []tls.CurveID { 108 | if cfg.Server.StrictCurves { 109 | return []tls.CurveID{ 110 | tls.CurveP521, 111 | tls.CurveP384, 112 | tls.CurveP256, 113 | } 114 | } 115 | 116 | return nil 117 | } 118 | 119 | // Ciphers provides optionally a list of secure ciphers. 120 | func Ciphers(cfg *config.Config) []uint16 { 121 | if cfg.Server.StrictCiphers { 122 | return []uint16{ 123 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 124 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 125 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 126 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func init() { 134 | chi.RegisterMethod("lock") 135 | chi.RegisterMethod("unlock") 136 | } 137 | -------------------------------------------------------------------------------- /pkg/version/collector.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | // Collector simply exports the version information for Prometheus. 8 | func Collector(ns string) *prometheus.GaugeVec { 9 | info := prometheus.NewGaugeVec( 10 | prometheus.GaugeOpts{ 11 | Namespace: ns, 12 | Name: "build_info", 13 | Help: "A metric with a constant '1' value labeled by version, revision and goversion from which it was built.", 14 | }, 15 | []string{"version", "revision", "goversion"}, 16 | ) 17 | 18 | info.WithLabelValues(String, Revision, Go).Set(1) 19 | return info 20 | } 21 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "runtime" 5 | ) 6 | 7 | var ( 8 | // String gets defined by the build system. 9 | String = "0.0.0-dev" 10 | 11 | // Revision indicates the commit this binary was built from. 12 | Revision string 13 | 14 | // Date indicates the date this binary was built. 15 | Date string 16 | 17 | // Go running this binary. 18 | Go = runtime.Version() 19 | ) 20 | -------------------------------------------------------------------------------- /reflex.conf: -------------------------------------------------------------------------------- 1 | # backend 2 | -r '^(cmd|pkg)/.*\.go$' -s -- sh -c 'make bin/terrastate-debug && bin/terrastate-debug server' 3 | -------------------------------------------------------------------------------- /revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.range] 20 | [rule.receiver-naming] 21 | [rule.time-naming] 22 | [rule.unexported-return] 23 | [rule.indent-error-flow] 24 | [rule.errorf] 25 | [rule.empty-block] 26 | [rule.superfluous-else] 27 | [rule.unused-parameter] 28 | [rule.unreachable-code] 29 | [rule.redefines-builtin-id] 30 | 31 | [rule.package-comments] 32 | Disabled = true 33 | 34 | --------------------------------------------------------------------------------