provider/gitlab: add gitlab provider and `gitlab_project` resource

Here we add a basic provider with a single resource type.

It's copied heavily from the `github` provider and `github_repository`
resource, as there is some overlap in those types/apis.

~~~
resource "gitlab_project" "test1" {
  name = "test1"
  visibility_level = "public"
}
~~~

We implement in terms of the
[go-gitlab](https://github.com/xanzy/go-gitlab) library, which provides
a wrapping of the [gitlab api](https://docs.gitlab.com/ee/api/)

We have been a little selective in the properties we surface for the
project resource, as not all properties are very instructive.
Notable is the removal of the `public` bool as the `visibility_level`
will take precedent if both are supplied which leads to confusing
interactions if they disagree.
This commit is contained in:
Richard Clamp 2016-10-19 15:25:12 +01:00
parent 1a21a388f5
commit 631b0b865c
11 changed files with 742 additions and 0 deletions

View File

@ -0,0 +1,12 @@
package main
import (
"github.com/hashicorp/terraform/builtin/providers/gitlab"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: gitlab.Provider,
})
}

View File

@ -0,0 +1,31 @@
package gitlab
import (
"github.com/xanzy/go-gitlab"
)
// Config is per-provider, specifies where to connect to gitlab
type Config struct {
Token string
BaseURL string
}
// Client returns a *gitlab.Client to interact with the configured gitlab instance
func (c *Config) Client() (interface{}, error) {
client := gitlab.NewClient(nil, c.Token)
if c.BaseURL != "" {
err := client.SetBaseURL(c.BaseURL)
if err != nil {
// The BaseURL supplied wasn't valid, bail.
return nil, err
}
}
// Test the credentials by checking we can get information about the authenticated user.
_, _, err := client.Users.CurrentUser()
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -0,0 +1,52 @@
package gitlab
import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
// Provider returns a terraform.ResourceProvider.
func Provider() terraform.ResourceProvider {
// The actual provider
return &schema.Provider{
Schema: map[string]*schema.Schema{
"token": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("GITLAB_TOKEN", nil),
Description: descriptions["token"],
},
"base_url": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("GITLAB_BASE_URL", ""),
Description: descriptions["base_url"],
},
},
ResourcesMap: map[string]*schema.Resource{
"gitlab_project": resourceGitlabProject(),
},
ConfigureFunc: providerConfigure,
}
}
var descriptions map[string]string
func init() {
descriptions = map[string]string{
"token": "The OAuth token used to connect to GitLab.",
"base_url": "The GitLab Base API URL",
}
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
Token: d.Get("token").(string),
BaseURL: d.Get("base_url").(string),
}
return config.Client()
}

View File

@ -0,0 +1,35 @@
package gitlab
import (
"os"
"testing"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider
func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"gitlab": testAccProvider,
}
}
func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("GITLAB_TOKEN"); v == "" {
t.Fatal("GITLAB_TOKEN must be set for acceptance tests")
}
}

View File

@ -0,0 +1,207 @@
package gitlab
import (
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
gitlab "github.com/xanzy/go-gitlab"
)
func resourceGitlabProject() *schema.Resource {
return &schema.Resource{
Create: resourceGitlabProjectCreate,
Read: resourceGitlabProjectRead,
Update: resourceGitlabProjectUpdate,
Delete: resourceGitlabProjectDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"default_branch": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"issues_enabled": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"merge_requests_enabled": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"wiki_enabled": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"snippets_enabled": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"visibility_level": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateValueFunc([]string{"private", "internal", "public"}),
Default: "private",
},
"ssh_url_to_repo": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"http_url_to_repo": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"web_url": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceGitlabProjectUpdateFromAPI(d *schema.ResourceData, project *gitlab.Project) {
d.Set("name", project.Name)
d.Set("description", project.Description)
d.Set("default_branch", project.DefaultBranch)
d.Set("issues_enabled", project.IssuesEnabled)
d.Set("merge_requests_enabled", project.MergeRequestsEnabled)
d.Set("wiki_enabled", project.WikiEnabled)
d.Set("snippets_enabled", project.SnippetsEnabled)
d.Set("visibility_level", visibilityLevelToString(project.VisibilityLevel))
d.Set("ssh_url_to_repo", project.SSHURLToRepo)
d.Set("http_url_to_repo", project.HTTPURLToRepo)
d.Set("web_url", project.WebURL)
}
func resourceGitlabProjectCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*gitlab.Client)
options := &gitlab.CreateProjectOptions{
Name: gitlab.String(d.Get("name").(string)),
}
if v, ok := d.GetOk("description"); ok {
options.Description = gitlab.String(v.(string))
}
if v, ok := d.GetOk("issues_enabled"); ok {
options.IssuesEnabled = gitlab.Bool(v.(bool))
}
if v, ok := d.GetOk("merge_requests_enabled"); ok {
options.MergeRequestsEnabled = gitlab.Bool(v.(bool))
}
if v, ok := d.GetOk("wiki_enabled"); ok {
options.WikiEnabled = gitlab.Bool(v.(bool))
}
if v, ok := d.GetOk("snippets_enabled"); ok {
options.SnippetsEnabled = gitlab.Bool(v.(bool))
}
if v, ok := d.GetOk("visibility_level"); ok {
options.VisibilityLevel = stringToVisibilityLevel(v.(string))
}
log.Printf("[DEBUG] create gitlab project %q", options.Name)
project, _, err := client.Projects.CreateProject(options)
if err != nil {
return err
}
d.SetId(fmt.Sprintf("%d", project.ID))
resourceGitlabProjectUpdateFromAPI(d, project)
return nil
}
func resourceGitlabProjectRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*gitlab.Client)
log.Printf("[DEBUG] read gitlab project %s", d.Id())
project, response, err := client.Projects.GetProject(d.Id())
if err != nil {
if response.StatusCode == 404 {
log.Printf("[WARN] removing project %s from state because it no longer exists in gitlab", d.Id())
d.SetId("")
return nil
}
return err
}
resourceGitlabProjectUpdateFromAPI(d, project)
return nil
}
func resourceGitlabProjectUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*gitlab.Client)
options := &gitlab.EditProjectOptions{}
if d.HasChange("name") {
options.Name = gitlab.String(d.Get("name").(string))
}
if d.HasChange("description") {
options.Description = gitlab.String(d.Get("description").(string))
}
if d.HasChange("default_branch") {
options.DefaultBranch = gitlab.String(d.Get("description").(string))
}
if d.HasChange("visibility_level") {
options.VisibilityLevel = stringToVisibilityLevel(d.Get("visibility_level").(string))
}
if d.HasChange("issues_enabled") {
options.IssuesEnabled = gitlab.Bool(d.Get("issues_enabled").(bool))
}
if d.HasChange("merge_requests_enabled") {
options.MergeRequestsEnabled = gitlab.Bool(d.Get("merge_requests_enabled").(bool))
}
if d.HasChange("wiki_enabled") {
options.WikiEnabled = gitlab.Bool(d.Get("wiki_enabled").(bool))
}
if d.HasChange("snippets_enabled") {
options.SnippetsEnabled = gitlab.Bool(d.Get("snippets_enabled").(bool))
}
log.Printf("[DEBUG] update gitlab project %s", d.Id())
project, _, err := client.Projects.EditProject(d.Id(), options)
if err != nil {
return err
}
resourceGitlabProjectUpdateFromAPI(d, project)
return nil
}
func resourceGitlabProjectDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*gitlab.Client)
log.Printf("[DEBUG] update gitlab project %s", d.Id())
_, err := client.Projects.DeleteProject(d.Id())
return err
}

View File

@ -0,0 +1,185 @@
package gitlab
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/xanzy/go-gitlab"
)
func TestAccGitlabProject_basic(t *testing.T) {
var project gitlab.Project
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckGitlabProjectDestroy,
Steps: []resource.TestStep{
// Create a project with all the features on
resource.TestStep{
Config: testAccGitlabProjectConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckGitlabProjectExists("gitlab_project.foo", &project),
testAccCheckGitlabProjectAttributes(&project, &testAccGitlabProjectExpectedAttributes{
Name: "foo",
Description: "Terraform acceptance tests",
IssuesEnabled: true,
MergeRequestsEnabled: true,
WikiEnabled: true,
SnippetsEnabled: true,
VisibilityLevel: 20,
}),
),
},
// Update the project to turn the features off
resource.TestStep{
Config: testAccGitlabProjectUpdateConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckGitlabProjectExists("gitlab_project.foo", &project),
testAccCheckGitlabProjectAttributes(&project, &testAccGitlabProjectExpectedAttributes{
Name: "foo",
Description: "Terraform acceptance tests!",
VisibilityLevel: 20,
}),
),
},
// Update the project to turn the features on again
resource.TestStep{
Config: testAccGitlabProjectConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckGitlabProjectExists("gitlab_project.foo", &project),
testAccCheckGitlabProjectAttributes(&project, &testAccGitlabProjectExpectedAttributes{
Name: "foo",
Description: "Terraform acceptance tests",
IssuesEnabled: true,
MergeRequestsEnabled: true,
WikiEnabled: true,
SnippetsEnabled: true,
VisibilityLevel: 20,
}),
),
},
},
})
}
func testAccCheckGitlabProjectExists(n string, project *gitlab.Project) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not Found: %s", n)
}
repoName := rs.Primary.ID
if repoName == "" {
return fmt.Errorf("No project ID is set")
}
conn := testAccProvider.Meta().(*gitlab.Client)
gotProject, _, err := conn.Projects.GetProject(repoName)
if err != nil {
return err
}
*project = *gotProject
return nil
}
}
type testAccGitlabProjectExpectedAttributes struct {
Name string
Description string
DefaultBranch string
IssuesEnabled bool
MergeRequestsEnabled bool
WikiEnabled bool
SnippetsEnabled bool
VisibilityLevel gitlab.VisibilityLevelValue
}
func testAccCheckGitlabProjectAttributes(project *gitlab.Project, want *testAccGitlabProjectExpectedAttributes) resource.TestCheckFunc {
return func(s *terraform.State) error {
if project.Name != want.Name {
return fmt.Errorf("got repo %q; want %q", project.Name, want.Name)
}
if project.Description != want.Description {
return fmt.Errorf("got description %q; want %q", project.Description, want.Description)
}
if project.DefaultBranch != want.DefaultBranch {
return fmt.Errorf("got default_branch %q; want %q", project.DefaultBranch, want.DefaultBranch)
}
if project.IssuesEnabled != want.IssuesEnabled {
return fmt.Errorf("got issues_enabled %t; want %t", project.IssuesEnabled, want.IssuesEnabled)
}
if project.MergeRequestsEnabled != want.MergeRequestsEnabled {
return fmt.Errorf("got merge_requests_enabled %t; want %t", project.MergeRequestsEnabled, want.MergeRequestsEnabled)
}
if project.WikiEnabled != want.WikiEnabled {
return fmt.Errorf("got wiki_enabled %t; want %t", project.WikiEnabled, want.WikiEnabled)
}
if project.SnippetsEnabled != want.SnippetsEnabled {
return fmt.Errorf("got snippets_enabled %t; want %t", project.SnippetsEnabled, want.SnippetsEnabled)
}
if project.VisibilityLevel != want.VisibilityLevel {
return fmt.Errorf("got default branch %q; want %q", project.VisibilityLevel, want.VisibilityLevel)
}
return nil
}
}
func testAccCheckGitlabProjectDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*gitlab.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "gitlab_project" {
continue
}
gotRepo, resp, err := conn.Projects.GetProject(rs.Primary.ID)
if err == nil {
if gotRepo != nil && fmt.Sprintf("%d", gotRepo.ID) == rs.Primary.ID {
return fmt.Errorf("Repository still exists")
}
}
if resp.StatusCode != 404 {
return err
}
return nil
}
return nil
}
const testAccGitlabProjectConfig = `
resource "gitlab_project" "foo" {
name = "foo"
description = "Terraform acceptance tests"
# So that acceptance tests can be run in a gitlab organization
# with no billing
visibility_level = "public"
}
`
const testAccGitlabProjectUpdateConfig = `
resource "gitlab_project" "foo" {
name = "foo"
description = "Terraform acceptance tests!"
# So that acceptance tests can be run in a gitlab organization
# with no billing
visibility_level = "public"
issues_enabled = false
merge_requests_enabled = false
wiki_enabled = false
snippets_enabled = false
}
`

View File

@ -0,0 +1,54 @@
package gitlab
import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
gitlab "github.com/xanzy/go-gitlab"
)
// copied from ../github/util.go
func validateValueFunc(values []string) schema.SchemaValidateFunc {
return func(v interface{}, k string) (we []string, errors []error) {
value := v.(string)
valid := false
for _, role := range values {
if value == role {
valid = true
break
}
}
if !valid {
errors = append(errors, fmt.Errorf("%s is an invalid value for argument %s", value, k))
}
return
}
}
func stringToVisibilityLevel(s string) *gitlab.VisibilityLevelValue {
lookup := map[string]gitlab.VisibilityLevelValue{
"private": gitlab.PrivateVisibility,
"internal": gitlab.InternalVisibility,
"public": gitlab.PublicVisibility,
}
value, ok := lookup[s]
if !ok {
return nil
}
return &value
}
func visibilityLevelToString(v gitlab.VisibilityLevelValue) *string {
lookup := map[gitlab.VisibilityLevelValue]string{
gitlab.PrivateVisibility: "private",
gitlab.InternalVisibility: "internal",
gitlab.PublicVisibility: "public",
}
value, ok := lookup[v]
if !ok {
return nil
}
return &value
}

View File

@ -0,0 +1,65 @@
package gitlab
import (
"testing"
"github.com/xanzy/go-gitlab"
)
func TestGitlab_validation(t *testing.T) {
cases := []struct {
Value string
ErrCount int
}{
{
Value: "invalid",
ErrCount: 1,
},
{
Value: "valid_one",
ErrCount: 0,
},
{
Value: "valid_two",
ErrCount: 0,
},
}
validationFunc := validateValueFunc([]string{"valid_one", "valid_two"})
for _, tc := range cases {
_, errors := validationFunc(tc.Value, "test_arg")
if len(errors) != tc.ErrCount {
t.Fatalf("Expected 1 validation error")
}
}
}
func TestGitlab_visbilityHelpers(t *testing.T) {
cases := []struct {
String string
Level gitlab.VisibilityLevelValue
}{
{
String: "private",
Level: gitlab.PrivateVisibility,
},
{
String: "public",
Level: gitlab.PublicVisibility,
},
}
for _, tc := range cases {
level := stringToVisibilityLevel(tc.String)
if level == nil || *level != tc.Level {
t.Fatalf("got %v expected %v", level, tc.Level)
}
sv := visibilityLevelToString(tc.Level)
if sv == nil || *sv != tc.String {
t.Fatalf("got %v expected %v", sv, tc.String)
}
}
}

View File

@ -31,6 +31,7 @@ import (
externalprovider "github.com/hashicorp/terraform/builtin/providers/external"
fastlyprovider "github.com/hashicorp/terraform/builtin/providers/fastly"
githubprovider "github.com/hashicorp/terraform/builtin/providers/github"
gitlabprovider "github.com/hashicorp/terraform/builtin/providers/gitlab"
googleprovider "github.com/hashicorp/terraform/builtin/providers/google"
grafanaprovider "github.com/hashicorp/terraform/builtin/providers/grafana"
herokuprovider "github.com/hashicorp/terraform/builtin/providers/heroku"
@ -107,6 +108,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{
"external": externalprovider.Provider,
"fastly": fastlyprovider.Provider,
"github": githubprovider.Provider,
"gitlab": gitlabprovider.Provider,
"google": googleprovider.Provider,
"grafana": grafanaprovider.Provider,
"heroku": herokuprovider.Provider,

View File

@ -0,0 +1,41 @@
---
layout: "gitlab"
page_title: "Provider: GitLab"
sidebar_current: "docs-gitlab-index"
description: |-
The GitLab provider is used to interact with GitLab organization resources.
---
# GitLab Provider
The GitLab provider is used to interact with GitLab organization resources.
The provider allows you to manage your GitLab organization's members and teams easily.
It needs to be configured with the proper credentials before it can be used.
Use the navigation to the left to read about the available resources.
## Example Usage
```
# Configure the GitLab Provider
provider "gitlab" {
token = "${var.github_token}"
}
# Add a project to the organization
resource "gitlab_project" "sample_project" {
...
}
```
## Argument Reference
The following arguments are supported in the `provider` block:
* `token` - (Optional) This is the GitLab personal access token. It must be provided, but
it can also be sourced from the `GITLAB_TOKEN` environment variable.
* `base_url` - (Optional) This is the target GitLab base API endpoint. Providing a value is a
requirement when working with GitLab CE or GitLab Enterprise. It is optional to provide this value and
it can also be sourced from the `GITLAB_BASE_URL` environment variable. The value must end with a slash.

View File

@ -0,0 +1,58 @@
---
layout: "gitlab"
page_title: "GitLab: gitlab_project"
sidebar_current: "docs-gitlab-resource-project"
description: |-
Creates and manages projects within Github organizations
---
# gitlab\_project
This resource allows you to create and manage projects within your
GitLab organization.
## Example Usage
```
resource "gitlab_repository" "example" {
name = "example"
description = "My awesome codebase"
visbility_level = "public"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the project.
* `description` - (Optional) A description of the project.
* `default_branch` - (Optional) The default branch for the project.
* `issues_enabled` - (Optional) Enable issue tracking for the project.
* `merge_requests_enabled` - (Optional) Enable merge requests for the project.
* `wiki_enabled` - (Optional) Enable wiki for the project.
* `snippets_enabled` - (Optional) Enable snippets for the project.
* `visbility_level` - (Optional) Set to `public` to create a public project.
Valid values are `private`, `internal`, `public`.
Repositories are created as private by default.
## Attributes Reference
The following additional attributes are exported:
* `ssh_url_to_repo` - URL that can be provided to `git clone` to clone the
repository via SSH.
* `http_url_to_repo` - URL that can be provided to `git clone` to clone the
repository via HTTP.
* `web_url` - URL that can be used to find the project in a browser.