Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for GitLab as terraform state backend provider #130

Merged
merged 2 commits into from
Jan 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 24 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ Terraboard is a web dashboard to visualize and query
- a search interface to query resources by type, name or attributes
- a diff interface to compare state between versions

It currently supports S3 as a remote state backend, and dynamoDB for
retrieving lock informations. Also supports Terraform Cloud (in past Terraform Enterprise [more](https://www.terraform.io/docs/cloud/index.html#note-about-product-names))
It currently supports several remote state backend providers:

- [AWS S3 (state) + DynamoDB (lock)](https://www.terraform.io/docs/backends/types/s3.html)
- [Google Cloud Storage](https://www.terraform.io/docs/backends/types/gcs.html)
- [Terraform Cloud (remote)](https://www.terraform.io/docs/backends/types/remote.html)
- [GitLab](https://docs.gitlab.com/ee/user/infrastructure/terraform_state.html)

### Overview

Expand Down Expand Up @@ -62,26 +65,22 @@ version.

### Requirements

Terraboard currently supports getting the Terraform states from AWS S3 and Terraform Cloud (in past Terraform Enterprise [more](https://www.terraform.io/docs/cloud/index.html#note-about-product-names)). It
requires:
- Terraform states from AWS S3:
* A **versioned** S3 bucket name with one or more Terraform states,
named with a `.tfstate` suffix
* AWS credentials with the following rights on the bucket:
- `s3:GetObject`
- `s3:ListBucket`
- `s3:ListBucketVersions`
- `s3:GetObjectVersion`
* If you want to retrieve lock states
[from a dynamoDB table](https://www.terraform.io/docs/backends/types/s3.html#dynamodb_table),
you need to make sure the provided AWS credentials have `dynamodb:Scan` access to that
table.
- Terraform states from Terraform Cloud:
* Account on [Terraform Cloud](https://app.terraform.io/)
* Existing organization
* Token assigned to an organization
- A running PostgreSQL database
Independently of the location of your statefiles, Terraboard needs to store an internal version of its dataset. For this purpose it requires a PostgreSQL database.
Data resiliency is not paramount though as this dataset can be rebuilt upon your statefiles at anytime.
#### AWS S3 (state) + DynamoDB (lock)

- A **versioned** S3 bucket name with one or more Terraform states, named with a `.tfstate` suffix
- AWS credentials with the following IAM permissions over the bucket:
- `s3:GetObject`
- `s3:ListBucket`
- `s3:ListBucketVersions`
- `s3:GetObjectVersion`
- If you want to retrieve lock states [from a dynamoDB table](https://www.terraform.io/docs/backends/types/s3.html#dynamodb_table), you need to make sure the provided AWS credentials have `dynamodb:Scan` access to that table.
#### Terraform Cloud

- Account on [Terraform Cloud](https://app.terraform.io/)
- Existing organization
- Token assigned to an organization

## Configuration

Expand Down Expand Up @@ -230,10 +229,10 @@ Terraboard is made of two components:

The server is written in go and runs a web server which serves:

- the API on known access points, taking the data from the PostgreSQL
database
- the index page (from [static/index.html](static/index.html)) on all other
URLs
- the API on known access points, taking the data from the PostgreSQL
database
- the index page (from [static/index.html](static/index.html)) on all other
URLs

The server also has a routine which regularly (every 1 minute) feeds
the PostgreSQL database from the S3 bucket.
Expand All @@ -244,7 +243,6 @@ The UI is an AngularJS application served from `index.html`. All the UI code
can be found in the [static/](static/) directory.



### Testing

```shell
Expand All @@ -254,5 +252,4 @@ $ docker-compose build && docker-compose up -d

### Contributing


See [CONTRIBUTING.md](CONTRIBUTING.md)
10 changes: 9 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,18 @@ type TFEConfig struct {
Organization string `long:"tfe-organization" env:"TFE_ORGANIZATION" yaml:"tfe-organization" description:"Terraform Enterprise organization for states access"`
}

// GCPConfig stores the Google Cloud configutation
// GCPConfig stores the Google Cloud configuration
type GCPConfig struct {
GCSBuckets []string `long:"gcs-bucket" yaml:"gcs-bucket" description:"Google Cloud bucket to search"`
GCPSAKey string `long:"gcp-sa-key-path" env:"GCP_SA_KEY_PATH" yaml:"gcp-sa-key-path" description:"The path to the service account to use to connect to Google Cloud Platform"`
}

// GitlabConfig stores the GitLab configuration
type GitlabConfig struct {
Address string `long:"gitlab-address" env:"GITLAB_ADDRESS" yaml:"gitlab-address" description:"GitLab address (root)" default:"https://gitlab.com"`
Token string `long:"gitlab-token" env:"GITLAB_TOKEN" yaml:"gitlab-token" description:"Token to authenticate upon GitLab"`
}

// WebConfig stores the UI interface parameters
type WebConfig struct {
Port uint16 `short:"p" long:"port" env:"TERRABOARD_PORT" yaml:"port" description:"Port to listen on." default:"8080"`
Expand All @@ -81,6 +87,8 @@ type Config struct {

GCP GCPConfig `group:"Google Cloud Platform Options" yaml:"gcp"`

Gitlab GitlabConfig `group:"GitLab Options" yaml:"gitlab"`

Web WebConfig `group:"Web" yaml:"web"`
}

Expand Down
16 changes: 9 additions & 7 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,19 @@ func TestLoadConfigFromYaml(t *testing.T) {
FileExtension: ".tfstate",
},
},
TFE: TFEConfig{
Address: "https://tfe.example.com",
Token: "foo",
Organization: "bar",
},
GCP: GCPConfig{
GCSBuckets: []string{"my-bucket-1", "my-bucket-2"},
GCPSAKey: "/path/to/key",
},
Gitlab: GitlabConfig{
Address: "https://gitlab.example.com",
Token: "foo",
},
Web: WebConfig{
Port: 39090,
BaseURL: "/test/",
Expand All @@ -52,7 +61,6 @@ func TestSetLogging_debug(t *testing.T) {
c.Log.Level = "debug"
c.Log.Format = "plain"
err := c.SetupLogging()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
Expand All @@ -67,7 +75,6 @@ func TestSetLogging_info(t *testing.T) {
c.Log.Level = "info"
c.Log.Format = "plain"
err := c.SetupLogging()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
Expand All @@ -82,7 +89,6 @@ func TestSetLogging_warn(t *testing.T) {
c.Log.Level = "warn"
c.Log.Format = "plain"
err := c.SetupLogging()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
Expand All @@ -97,7 +103,6 @@ func TestSetLogging_error(t *testing.T) {
c.Log.Level = "error"
c.Log.Format = "plain"
err := c.SetupLogging()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
Expand All @@ -112,7 +117,6 @@ func TestSetLogging_fatal(t *testing.T) {
c.Log.Level = "fatal"
c.Log.Format = "plain"
err := c.SetupLogging()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
Expand All @@ -127,7 +131,6 @@ func TestSetLogging_panic(t *testing.T) {
c.Log.Level = "panic"
c.Log.Format = "plain"
err := c.SetupLogging()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
Expand Down Expand Up @@ -159,7 +162,6 @@ func TestSetLogging_json(t *testing.T) {
c.Log.Level = "debug"
c.Log.Format = "json"
err := c.SetupLogging()

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
Expand Down
9 changes: 9 additions & 0 deletions config/config_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@ aws:
key-prefix: test/
file-extension: .tfstate

tfe:
address: https://tfe.example.com
token: foo
organisation: bar

gcp:
gcs-bucket:
- my-bucket-1
- my-bucket-2
gcp-sa-key-path: /path/to/key

gitlab:
address: https://gitlab.example.com
token: foo

web:
port: 39090
base-url: /test/
Expand Down
5 changes: 2 additions & 3 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ func (db *Database) stateS3toDB(sf *statefile.File, path string, versionID strin
}
mod.Resources = append(mod.Resources, res)
}

}
st.Modules = append(st.Modules, mod)
}
Expand Down Expand Up @@ -409,8 +408,8 @@ func (db *Database) ListResourceTypes() ([]string, error) {
return db.listField("resources", "type")
}

//ListResourceTypesWithCount returns a list of Resource types with associated counts
//from the Database
// ListResourceTypesWithCount returns a list of Resource types with associated counts
// from the Database
func (db *Database) ListResourceTypesWithCount() (results []map[string]string, err error) {
sql := "SELECT resources.type, COUNT(*)" +
" FROM (SELECT DISTINCT ON(states.path) states.id, states.path, states.serial, states.tf_version, versions.version_id, versions.last_modified" +
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/jessevdk/go-flags v1.4.0
github.com/jinzhu/gorm v1.9.16
github.com/lib/pq v1.8.0 // indirect
github.com/machinebox/graphql v0.2.2
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/pmezard/go-difflib v1.0.0
github.com/sirupsen/logrus v1.6.0
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,8 @@ github.com/likexian/simplejson-go v0.0.0-20190409170913-40473a74d76d/go.mod h1:T
github.com/likexian/simplejson-go v0.0.0-20190419151922-c1f9f0b4f084/go.mod h1:U4O1vIJvIKwbMZKUJ62lppfdvkCdVd2nfMimHK81eec=
github.com/likexian/simplejson-go v0.0.0-20190502021454-d8787b4bfa0b/go.mod h1:3BWwtmKP9cXWwYCr5bkoVDEfLywacOv0s06OBEDpyt8=
github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82/go.mod h1:y54tfGmO3NKssKveTEFFzH8C/akrSOy/iW9qEAUDV84=
github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo=
github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc=
github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc=
Expand Down Expand Up @@ -397,6 +399,7 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func getVersion(w http.ResponseWriter, r *http.Request) {

j, err := json.Marshal(map[string]string{
"version": version,
"copyright": "Copyright © 2017-2020 Camptocamp",
"copyright": "Copyright © 2017-2021 Camptocamp",
})
if err != nil {
api.JSONError(w, "Failed to marshal version", err)
Expand Down
Loading