diff --git a/builtin/bins/provider-gitlab/main.go b/builtin/bins/provider-gitlab/main.go new file mode 100644 index 000000000..acb94705d --- /dev/null +++ b/builtin/bins/provider-gitlab/main.go @@ -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, + }) +} diff --git a/builtin/providers/gitlab/config.go b/builtin/providers/gitlab/config.go new file mode 100644 index 000000000..288f7ba6a --- /dev/null +++ b/builtin/providers/gitlab/config.go @@ -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 +} diff --git a/builtin/providers/gitlab/provider.go b/builtin/providers/gitlab/provider.go new file mode 100644 index 000000000..f4389d694 --- /dev/null +++ b/builtin/providers/gitlab/provider.go @@ -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() +} diff --git a/builtin/providers/gitlab/provider_test.go b/builtin/providers/gitlab/provider_test.go new file mode 100644 index 000000000..a28eddb8d --- /dev/null +++ b/builtin/providers/gitlab/provider_test.go @@ -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") + } +} diff --git a/builtin/providers/gitlab/resource_gitlab_project.go b/builtin/providers/gitlab/resource_gitlab_project.go new file mode 100644 index 000000000..b4824f36a --- /dev/null +++ b/builtin/providers/gitlab/resource_gitlab_project.go @@ -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 +} diff --git a/builtin/providers/gitlab/resource_gitlab_project_test.go b/builtin/providers/gitlab/resource_gitlab_project_test.go new file mode 100644 index 000000000..d4b53e476 --- /dev/null +++ b/builtin/providers/gitlab/resource_gitlab_project_test.go @@ -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 +} +` diff --git a/builtin/providers/gitlab/util.go b/builtin/providers/gitlab/util.go new file mode 100644 index 000000000..942e30852 --- /dev/null +++ b/builtin/providers/gitlab/util.go @@ -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 +} diff --git a/builtin/providers/gitlab/util_test.go b/builtin/providers/gitlab/util_test.go new file mode 100644 index 000000000..465eec73c --- /dev/null +++ b/builtin/providers/gitlab/util_test.go @@ -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) + } + } +} diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index 2f48908c7..cfcf5e37c 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -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, diff --git a/website/source/docs/providers/gitlab/index.html.markdown b/website/source/docs/providers/gitlab/index.html.markdown new file mode 100644 index 000000000..cf6fd431a --- /dev/null +++ b/website/source/docs/providers/gitlab/index.html.markdown @@ -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. diff --git a/website/source/docs/providers/gitlab/r/project.html.markdown b/website/source/docs/providers/gitlab/r/project.html.markdown new file mode 100644 index 000000000..4d807c960 --- /dev/null +++ b/website/source/docs/providers/gitlab/r/project.html.markdown @@ -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.