From 8b92a25ac153fe7b59a646546c5654f1851ef43e Mon Sep 17 00:00:00 2001 From: Maxime VISONNEAU Date: Tue, 12 Jan 2021 18:04:34 +0000 Subject: [PATCH 1/2] Added support for GitLab as terraform state backend provider --- README.md | 51 +++--- config/config.go | 10 +- config/config_test.go | 16 +- config/config_test.yml | 9 ++ db/db.go | 5 +- go.mod | 1 + go.sum | 3 + main.go | 2 +- pkg/client/gitlab/gitlab.go | 268 +++++++++++++++++++++++++++++++ pkg/client/gitlab/gitlab_test.go | 3 + state/gcp.go | 6 +- state/gitlab.go | 117 ++++++++++++++ state/gitlab_test.go | 3 + state/state.go | 17 +- state/tfe.go | 6 +- 15 files changed, 462 insertions(+), 55 deletions(-) create mode 100644 pkg/client/gitlab/gitlab.go create mode 100644 pkg/client/gitlab/gitlab_test.go create mode 100644 state/gitlab.go create mode 100644 state/gitlab_test.go diff --git a/README.md b/README.md index 496abe27..ed53be70 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 a 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 @@ -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. @@ -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 @@ -254,5 +252,4 @@ $ docker-compose build && docker-compose up -d ### Contributing - See [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/config/config.go b/config/config.go index c0a9c9fe..247fb1fd 100644 --- a/config/config.go +++ b/config/config.go @@ -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"` @@ -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"` } diff --git a/config/config_test.go b/config/config_test.go index f0bf407e..1aeb9e38 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -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/", @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } @@ -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) } diff --git a/config/config_test.yml b/config/config_test.yml index 727b5e0b..4f4866a7 100644 --- a/config/config_test.yml +++ b/config/config_test.yml @@ -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/ diff --git a/db/db.go b/db/db.go index 00be91fb..e316867d 100644 --- a/db/db.go +++ b/db/db.go @@ -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) } @@ -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" + diff --git a/go.mod b/go.mod index 93aaebf6..1537ebb0 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 2bb6c98e..619f3b08 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/main.go b/main.go index 28a9a37a..5a9032ed 100644 --- a/main.go +++ b/main.go @@ -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) diff --git a/pkg/client/gitlab/gitlab.go b/pkg/client/gitlab/gitlab.go new file mode 100644 index 00000000..5d2cdaea --- /dev/null +++ b/pkg/client/gitlab/gitlab.go @@ -0,0 +1,268 @@ +package gitlab + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/machinebox/graphql" +) + +// Client .. +type Client struct { + GraphQL *graphql.Client + Endpoint string + Token string +} + +// TerraformState .. +type TerraformState struct { + ID string + Name string + ProjectPathWithNamespace string + LatestVersion struct { + CreatedAt time.Time + CreatedBy string + Serial int + } + Lock *TerraformStateLock +} + +// TerraformStateLock .. +type TerraformStateLock struct { + CreatedAt time.Time + CreatedBy string +} + +// TerraformStates .. +type TerraformStates []TerraformState + +// Project .. +type Project struct { + ID string + PathWithNamespace string + TerraformStates TerraformStates +} + +// Projects .. +type Projects []Project + +const projectsQuery string = ` +query($first: Int, $after: String!) { + projects(first: $first, after: $after) { + pageInfo { + endCursor + hasNextPage + } + nodes { + id + fullPath + terraformStates { + count + } + } + } +}` + +const projectTerraformStatesQuery string = ` +query($fullPath: ID!, $first: Int, $after: String!){ + project(fullPath: $fullPath) { + terraformStates(first: $first, after: $after) { + pageInfo { + endCursor + hasNextPage + } + nodes { + id + name + lockedAt + lockedByUser { + publicEmail + } + latestVersion { + serial + createdAt + createdByUser { + publicEmail + } + } + } + } + } +}` + +// ProjectsResponse .. +type ProjectsResponse struct { + Projects struct { + PageInfo struct { + EndCursor string `json:"endCursor"` + HasNextPage bool `json:"hasNextPage"` + } `json:"pageInfo"` + Nodes []struct { + ID string `json:"id"` + FullPath string `json:"fullPath"` + TerraformStates struct { + Count int `json:"count"` + } `json:"terraformStates"` + } `json:"nodes"` + } `json:"projects"` +} + +// ProjectTerraformStatesResponse .. +type ProjectTerraformStatesResponse struct { + Project struct { + TerraformStates struct { + PageInfo struct { + EndCursor string `json:"endCursor"` + HasNextPage bool `json:"hasNextPage"` + } `json:"pageInfo"` + Nodes []struct { + ID string `json:"id"` + Name string `json:"name"` + LockedAt *time.Time `json:"lockedAt"` + LockedByUser *struct { + PublicEmail string `json:"publicEmail"` + } `json:"lockedByUser"` + LatestVersion struct { + Serial int `json:"serial"` + CreatedAt time.Time `json:"createdAt"` + CreatedByUser struct { + PublicEmail string `json:"publicEmail"` + } `json:"createdByUser"` + } `json:"latestVersion"` + } `json:"nodes"` + } `json:"terraformStates"` + } `json:"project"` +} + +// NewClient returns a new Client +func NewClient(endpoint, token string) Client { + return Client{ + GraphQL: graphql.NewClient(fmt.Sprintf("%s/api/graphql", endpoint)), + Endpoint: endpoint, + Token: token, + } +} + +// GetProjectsWithTerraformStates .. +func (c *Client) GetProjectsWithTerraformStates() (projects Projects, err error) { + resp := ProjectsResponse{} + vars := map[string]interface{}{ + "first": 50, + "after": "", + } + + for { + if err = c.Query(projectsQuery, &resp, vars); err != nil { + return + } + + for _, project := range resp.Projects.Nodes { + if project.TerraformStates.Count > 0 { + p := Project{ + ID: project.ID, + PathWithNamespace: project.FullPath, + } + + p.TerraformStates, err = c.GetProjectTerraformStates(project.FullPath) + if err != nil { + return + } + + projects = append(projects, p) + } + } + + if resp.Projects.PageInfo.HasNextPage { + vars["after"] = resp.Projects.PageInfo.EndCursor + continue + } + + break + } + + return +} + +// GetProjectTerraformStates .. +func (c *Client) GetProjectTerraformStates(pathWithNamespace string) (terraformStates TerraformStates, err error) { + resp := ProjectTerraformStatesResponse{} + vars := map[string]interface{}{ + "fullPath": pathWithNamespace, + "first": 50, + "after": "", + } + + for { + if err = c.Query(projectTerraformStatesQuery, &resp, vars); err != nil { + return + } + + for _, state := range resp.Project.TerraformStates.Nodes { + terraformState := TerraformState{ + ID: state.ID, + Name: state.Name, + ProjectPathWithNamespace: pathWithNamespace, + } + terraformState.LatestVersion.CreatedAt = state.LatestVersion.CreatedAt + terraformState.LatestVersion.CreatedBy = state.LatestVersion.CreatedByUser.PublicEmail + terraformState.LatestVersion.Serial = state.LatestVersion.Serial + + if state.LockedAt != nil { + terraformState.Lock = &TerraformStateLock{ + CreatedAt: *state.LockedAt, + } + + if state.LockedByUser != nil { + terraformState.Lock.CreatedBy = state.LockedByUser.PublicEmail + } + } + + terraformStates = append(terraformStates, terraformState) + } + + if resp.Project.TerraformStates.PageInfo.HasNextPage { + vars["after"] = resp.Project.TerraformStates.PageInfo.EndCursor + continue + } + + break + } + return +} + +// GlobalPath .. +func (s *TerraformState) GlobalPath() string { + return fmt.Sprintf("[%s] %s", s.ProjectPathWithNamespace, s.Name) +} + +// GetState .. +func (c *Client) GetState(projectID, stateName, version string) (state []byte, err error) { + var req *http.Request + var resp *http.Response + req, err = http.NewRequest("GET", fmt.Sprintf("%s/api/v4/projects/%s/terraform/state/%s/versions/%s", c.Endpoint, projectID, stateName, version), nil) + if err != nil { + return + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.Token)) + + resp, err = http.DefaultClient.Do(req) + if err != nil { + return + } + + state, err = ioutil.ReadAll(resp.Body) + return +} + +// Query .. +func (c *Client) Query(request string, response interface{}, vars map[string]interface{}) error { + req := graphql.NewRequest(request) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token)) + for k, v := range vars { + req.Var(k, v) + } + return c.GraphQL.Run(context.TODO(), req, response) +} diff --git a/pkg/client/gitlab/gitlab_test.go b/pkg/client/gitlab/gitlab_test.go new file mode 100644 index 00000000..f882ae91 --- /dev/null +++ b/pkg/client/gitlab/gitlab_test.go @@ -0,0 +1,3 @@ +package gitlab + +// TODO: implement! diff --git a/state/gcp.go b/state/gcp.go index add19401..31b50ba6 100644 --- a/state/gcp.go +++ b/state/gcp.go @@ -24,7 +24,7 @@ type GCP struct { } // NewGCP creates an GCP object -func NewGCP(c *config.Config) (GCP, error) { +func NewGCP(c *config.Config) (*GCP, error) { ctx := context.Background() var client *storage.Client var err error @@ -40,14 +40,14 @@ func NewGCP(c *config.Config) (GCP, error) { if err != nil { log.Fatalf("Failed to create client: %v", err) - return GCP{}, err + return nil, err } log.WithFields(log.Fields{ "buckets": c.GCP.GCSBuckets, }).Info("Client successfully created") - return GCP{ + return &GCP{ svc: client, buckets: c.GCP.GCSBuckets, }, nil diff --git a/state/gitlab.go b/state/gitlab.go new file mode 100644 index 00000000..fd855096 --- /dev/null +++ b/state/gitlab.go @@ -0,0 +1,117 @@ +package state + +import ( + "bytes" + "fmt" + "net/url" + "regexp" + "strconv" + + "github.com/camptocamp/terraboard/config" + "github.com/camptocamp/terraboard/pkg/client/gitlab" + "github.com/hashicorp/terraform/states/statefile" +) + +// Gitlab is a state provider type, leveraging GitLab +type Gitlab struct { + Client gitlab.Client +} + +// NewGitlab creates a new Gitlab object +func NewGitlab(c *config.Config) *Gitlab { + return &Gitlab{ + Client: gitlab.NewClient(c.Gitlab.Address, c.Gitlab.Token), + } +} + +// GetLocks returns a map of locks by State path +func (g *Gitlab) GetLocks() (locks map[string]LockInfo, err error) { + locks = make(map[string]LockInfo) + projects := gitlab.Projects{} + projects, err = g.Client.GetProjectsWithTerraformStates() + if err != nil { + return + } + + for _, project := range projects { + for _, state := range project.TerraformStates { + if state.Lock != nil { + locks[state.GlobalPath()] = LockInfo{ + ID: "N/A", + Operation: "N/A", + Info: "N/A", + Who: state.Lock.CreatedBy, + Version: "N/A", + Created: &state.Lock.CreatedAt, + Path: state.GlobalPath(), + } + } + } + } + + return +} + +// GetStates returns a slice of all found workspaces +func (g *Gitlab) GetStates() (states []string, err error) { + projects := gitlab.Projects{} + projects, err = g.Client.GetProjectsWithTerraformStates() + if err != nil { + return + } + + for _, project := range projects { + for _, state := range project.TerraformStates { + states = append(states, state.GlobalPath()) + } + } + + return +} + +// GetVersions returns a slice of Version objects +func (g *Gitlab) GetVersions(state string) (versions []Version, err error) { + projects := gitlab.Projects{} + projects, err = g.Client.GetProjectsWithTerraformStates() + if err != nil { + return + } + + for _, project := range projects { + for _, state := range project.TerraformStates { + for i := state.LatestVersion.Serial; i >= 0; i-- { + versions = append(versions, Version{ + ID: strconv.Itoa(i), + // TODO: Fix/implement once https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45851 will be released + // somehow it seems to be working correctly though, not sure from which place it manages to find the correct date + LastModified: state.LatestVersion.CreatedAt, + }) + } + } + } + + return +} + +// GetState retrieves a single state file from the GitLab API +func (g *Gitlab) GetState(path, version string) (sf *statefile.File, err error) { + re := regexp.MustCompile(`^\[(.*)] (.*)$`) + stateInfo := re.FindStringSubmatch(path) + if len(stateInfo) != 3 { + return nil, fmt.Errorf("invalid state path: %s", path) + } + + var state []byte + state, err = g.Client.GetState(url.PathEscape(stateInfo[1]), url.PathEscape(stateInfo[2]), version) + if err != nil { + return + } + + // Parse the statefile + sf, err = statefile.Read(bytes.NewReader(state)) + if sf == nil { + return nil, fmt.Errorf("Unable to parse the statefile for workspace %s version %s", path, version) + } + + return +} diff --git a/state/gitlab_test.go b/state/gitlab_test.go new file mode 100644 index 00000000..ac02f6ef --- /dev/null +++ b/state/gitlab_test.go @@ -0,0 +1,3 @@ +package state + +// TODO: implement! diff --git a/state/state.go b/state/state.go index 57605e09..6abcddcd 100644 --- a/state/state.go +++ b/state/state.go @@ -43,20 +43,17 @@ type Provider interface { func Configure(c *config.Config) (Provider, error) { if len(c.TFE.Token) > 0 { log.Info("Using Terraform Enterprise as the state/locks provider") - provider, err := NewTFE(c) - if err != nil { - return nil, err - } - return &provider, nil + return NewTFE(c) } if c.GCP.GCSBuckets != nil { log.Info("Using Google Cloud as the state/locks provider") - provider, err := NewGCP(c) - if err != nil { - return nil, err - } - return &provider, nil + return NewGCP(c) + } + + if len(c.Gitlab.Token) > 0 { + log.Info("Using Gitab as the state/locks provider") + return NewGitlab(c), nil } log.Info("Using AWS (S3+DynamoDB) as the state/locks provider") diff --git a/state/tfe.go b/state/tfe.go index fc9de0c4..301322b8 100644 --- a/state/tfe.go +++ b/state/tfe.go @@ -19,7 +19,7 @@ type TFE struct { } // NewTFE creates a new TFE object -func NewTFE(c *config.Config) (TFE, error) { +func NewTFE(c *config.Config) (*TFE, error) { config := &tfe.Config{ Address: c.TFE.Address, Token: c.TFE.Token, @@ -27,12 +27,12 @@ func NewTFE(c *config.Config) (TFE, error) { client, err := tfe.NewClient(config) if err != nil { - return TFE{}, err + return nil, err } ctx := context.Background() - return TFE{ + return &TFE{ Client: client, org: c.TFE.Organization, ctx: &ctx, From d7c62c6367bb9e8bcc3117ce258ce04b033cb429 Mon Sep 17 00:00:00 2001 From: Maxime VISONNEAU Date: Thu, 14 Jan 2021 16:32:29 +0000 Subject: [PATCH 2/2] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Raphaël Pinson --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed53be70..58b694db 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ version. ### Requirements -Independently of the location of your statefiles, Terraboard needs to store a internal version of its dataset. For this purpose it requires a 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)