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

Provide gitlab_pages_domain resource #1026

Closed
wants to merge 25 commits into from
Closed
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
48 changes: 48 additions & 0 deletions docs/resources/pages_domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "gitlab_pages_domain Resource - terraform-provider-gitlab"
subcategory: ""
description: |-
The gitlab_pages_domain resource allows to manage the lifecycle of a custom Pages domain including its TLS certificates.
Upstream API: GitLab REST API docs https://docs.gitlab.com/ee/api/pages_domains.html
---

# gitlab_pages_domain (Resource)

The `gitlab_pages_domain` resource allows to manage the lifecycle of a custom Pages domain including its TLS certificates.

**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/pages_domains.html)



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `domain` (String) The custom domain.
- `project` (String) The ID or [URL-encoded path of the project](https://docs.gitlab.com/ee/api/index.html#namespaced-path-encoding) owned by the authenticated user.

### Optional

- `auto_ssl_enabled` (Boolean) Enables automatic generation of SSL certificates issued by Let’s Encrypt for custom domains.
- `certificate` (String) The certificate in PEM format with intermediates following in most specific to least specific order.
- `certificate_data` (Block List, Max: 1) The certificate data. (see [below for nested schema](#nestedblock--certificate_data))
- `key` (String) The certificate key in PEM format.

### Read-Only

- `id` (String) The ID of this resource.
- `url` (String) The URL for the given domain.
- `verification_code` (String, Sensitive) The verification code for the domain.
- `verified` (Boolean) The certificate data.

<a id="nestedblock--certificate_data"></a>
### Nested Schema for `certificate_data`

Optional:

- `expiration` (String) The certificate expiration date.
- `expired` (Boolean) Is the certificate expired?


220 changes: 220 additions & 0 deletions internal/provider/resource_gitlab_pages_domain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package provider

import (
"context"
"log"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
gitlab "github.com/xanzy/go-gitlab"
)

var _ = registerResource("gitlab_pages_domain", func() *schema.Resource {
return &schema.Resource{
Description: `The ` + "`gitlab_pages_domain`" + ` resource allows to manage the lifecycle of a custom Pages domain including its TLS certificates.

**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/pages_domains.html)`,

CreateContext: resourceGitlabPagesDomainCreate,
ReadContext: resourceGitlabPagesDomainRead,
UpdateContext: resourceGitlabPagesDomainUpdate,
DeleteContext: resourceGitlabPagesDomainDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
"domain": {
Description: "The custom domain.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"project": {
Description: "The ID or [URL-encoded path of the project](https://docs.gitlab.com/ee/api/index.html#namespaced-path-encoding) owned by the authenticated user.",
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
"auto_ssl_enabled": {
Description: "Enables automatic generation of SSL certificates issued by Let’s Encrypt for custom domains.",
Type: schema.TypeBool,
Optional: true,
},
"certificate": {
Description: "The certificate in PEM format with intermediates following in most specific to least specific order.",
Type: schema.TypeString,
Optional: true,
},
"key": {
Description: "The certificate key in PEM format.",
Type: schema.TypeString,
Optional: true,
},
"url": {
Description: "The URL for the given domain.",
Type: schema.TypeString,
Computed: true,
},
"certificate_data": {
Description: "The certificate data.",
Type: schema.TypeList,
MaxItems: 1,
Computed: true,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"expired": {
nagyv marked this conversation as resolved.
Show resolved Hide resolved
Description: "Is the certificate expired?",
Type: schema.TypeBool,
Computed: true,
Optional: true,
},
"expiration": {
Description: "The certificate expiration date.",
Type: schema.TypeString,
ValidateFunc: validation.IsRFC3339Time,
Computed: true,
Optional: true,
},
},
},
},
"verified": {
Description: "The certificate data.",
Type: schema.TypeBool,
Computed: true,
},
"verification_code": {
Description: "The verification code for the domain.",
Type: schema.TypeString,
Computed: true,
Sensitive: true,
},
nagyv marked this conversation as resolved.
Show resolved Hide resolved
},
}
})

func resourceGitlabPagesDomainCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*gitlab.Client)
projectID := d.Get("project").(string)
domain := d.Get("domain").(string)
auto_ssl_enabled := d.Get("auto_ssl_enabled").(bool)
certificate := d.Get("certificate").(string)
key := d.Get("key").(string)

options := &gitlab.CreatePagesDomainOptions{
Domain: &domain,
AutoSslEnabled: &auto_ssl_enabled,
Certificate: &certificate,
Key: &key,
}
Comment on lines +103 to +113
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: to indicate that we only use these attributes for the GitLab options I think it makes sense to not have variables for them ...

Suggested change
domain := d.Get("domain").(string)
auto_ssl_enabled := d.Get("auto_ssl_enabled").(bool)
certificate := d.Get("certificate").(string)
key := d.Get("key").(string)
options := &gitlab.CreatePagesDomainOptions{
Domain: &domain,
AutoSslEnabled: &auto_ssl_enabled,
Certificate: &certificate,
Key: &key,
}
domain := d.Get("domain").(string)
auto_ssl_enabled := d.Get("auto_ssl_enabled").(bool)
certificate := d.Get("certificate").(string)
key := d.Get("key").(string)
options := &gitlab.CreatePagesDomainOptions{
Domain: gitlab.String(d.Get("domain").(string)),
AutoSslEnabled: gitlab.Bool(d.Get("auto_ssl_enabled").(bool))
Certificate: gitlab.String(d.Get("certificate").(string)),
Key: gitlab.String(d.Get("key").(string)),
}

Please verify the correct behavior during creation if these optional attributes are not given and that the GitLab API doesn't receive them ....

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean separate testcases? The current test does not specify any of the optional attributes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, noticed afterwards - let's make sure that both scenarios are tested: creation without optionals set, creation with optionals set ...


log.Printf("[DEBUG] create gitlab pages domain %s", domain)

_, _, err := client.PagesDomains.CreatePagesDomain(projectID, options, gitlab.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}

d.SetId(buildTwoPartID(&projectID, &domain))
return resourceGitlabPagesDomainRead(ctx, d, meta)
}

func resourceGitlabPagesDomainUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*gitlab.Client)

projectID, domain, err := parseTwoPartID(d.Id())
if err != nil {
return diag.FromErr(err)
}

options := &gitlab.UpdatePagesDomainOptions{}

if d.HasChange("auto_ssl_enabled") {
options.AutoSslEnabled = gitlab.Bool(d.Get("auto_ssl_enabled").(bool))
}
if d.HasChange("certificate") {
options.Certificate = gitlab.String(d.Get("certificate").(string))
}
if d.HasChange("key") {
options.Key = gitlab.String(d.Get("key").(string))
}

log.Printf("[DEBUG] update gitlab pages domain %s for %s", domain, projectID)

_, _, err2 := client.PagesDomains.UpdatePagesDomain(projectID, domain, options, gitlab.WithContext(ctx))
if err2 != nil {
return diag.FromErr(err2)
}
return resourceGitlabProjectMirrorRead(ctx, d, meta)
}

func resourceGitlabPagesDomainRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*gitlab.Client)
projectID, domain, err := parseTwoPartID(d.Id())
if err != nil {
return diag.FromErr(err)
}
log.Printf("[DEBUG] read gitlab pages domain %s", domain)

pagesDomain, _, err := client.PagesDomains.GetPagesDomain(projectID, domain, gitlab.WithContext(ctx))
if err != nil {
if is404(err) {
log.Printf("[DEBUG] gitlab pages domain %s not found, removing from state", domain)
d.SetId("")
return nil
}
return diag.FromErr(err)
}

d.Set("project", projectID)
d.Set("domain", pagesDomain.Domain)
d.Set("url", pagesDomain.URL)
d.Set("auto_ssl_enabled", pagesDomain.AutoSslEnabled)
if err := d.Set("certificate_data", flattenCertificateData(pagesDomain)); err != nil {
return diag.FromErr(err)
}
d.Set("verified", pagesDomain.Verified)
d.Set("verification_code", pagesDomain.VerificationCode)
return nil
}

func resourceGitlabPagesDomainDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*gitlab.Client)
projectID, domain, err := parseTwoPartID(d.Id())
if err != nil {
return diag.FromErr(err)
}
log.Printf("[DEBUG] Delete gitlab pages domain %s", domain)

_, err = client.PagesDomains.DeletePagesDomain(projectID, domain, gitlab.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}

return nil
}

func flattenCertificateData(pagesDomain *gitlab.PagesDomain) (certificate_data []map[string]interface{}) {
if pagesDomain == nil {
return
}

var certificate_expiration string
if pagesDomain.Certificate.Expiration == nil {
nagyv marked this conversation as resolved.
Show resolved Hide resolved
certificate_expiration = ""
} else {
certificate_expiration = pagesDomain.Certificate.Expiration.Format(time.RFC3339)
}

certificate_data = []map[string]interface{}{
{
"expired": pagesDomain.Certificate.Expired,
"expiration": certificate_expiration,
},
}
return certificate_data
}
119 changes: 119 additions & 0 deletions internal/provider/resource_gitlab_pages_domain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package provider

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/xanzy/go-gitlab"
)

func TestAccGitlabPagesDomain_basic(t *testing.T) {
var pagesDomain gitlab.PagesDomain
rInt := acctest.RandInt()
project := testAccCreateProject(t)

resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories,
CheckDestroy: testAccCheckGitlabPagesDestroy,
Steps: []resource.TestStep{
// Create a pages domain with all options
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add another test case to only supply the required data - there may be some issues in the current implementation when pagesDomain.Certificate is nil or even earlier when go-gitlab fails, because nil is passed to gitlab.String() - these are just suspicions though and everything might be okay as-is 👯

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the Default values and changed to testcase to include only the required ones.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per my other comment, let's have multiple test cases: testing w/ and w/o optionals set.

{
Config: testAccGitlabPagesDomainCreate(rInt, project.PathWithNamespace),
Check: resource.ComposeTestCheckFunc(
testAccCheckGitlabPagesDomainExists("gitlab_pages_domain.this", &pagesDomain),
resource.TestCheckResourceAttrSet("gitlab_pages_domain.this", "auto_ssl_enabled"),
Comment on lines +26 to +27
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the subsequent Verify import test step both of these checks are already done, because:

  1. import does a Read() and therefore the Pages domain actually has to exist
  2. it would result in a non-empty plan in case the auto_ssl_enabled is not properly set ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is your recommendation? I would prefer to have a "create" test case, beside the import.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

),
},
// Verify import
{
ResourceName: "gitlab_pages_domain.this",
ImportState: true,
ImportStateVerify: true,
},
// Update the pages domain to toggle all the values to their inverse
{
Config: testAccGitlabPagesDomainUpdate(rInt, project.PathWithNamespace),
Check: resource.ComposeTestCheckFunc(
testAccCheckGitlabPagesDomainExists("gitlab_pages_domain.this", &pagesDomain),
),
},
},
})
}

func testAccCheckGitlabPagesDomainExists(n string, pagesDomain *gitlab.PagesDomain) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not Found: %s", n)
}

projectID := rs.Primary.Attributes["project"]
if projectID == "" {
return fmt.Errorf("No project ID is set")
}

domain := rs.Primary.Attributes["domain"]
if domain == "" {
return fmt.Errorf("No domain is set")
}

gotPagesDomain, _, err := testGitlabClient.PagesDomains.GetPagesDomain(projectID, domain)
if err != nil {
return err
}
*pagesDomain = *gotPagesDomain
return nil
}
}

func testAccCheckGitlabPagesDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "gitlab_pages_domain" {
continue
}

projectID := rs.Primary.Attributes["project"]
if projectID == "" {
return fmt.Errorf("No project ID is set")
}

domain := rs.Primary.Attributes["domain"]
if domain == "" {
return fmt.Errorf("No domain is set")
}

gotPagesDomain, err := testGitlabClient.PagesDomains.DeletePagesDomain(projectID, domain)
if err == nil {
if gotPagesDomain != nil {
return fmt.Errorf("Pages domain %s still exists after deletion", domain)
}
}
if !is404(err) {
return err
}
return nil
}
return nil
}

func testAccGitlabPagesDomainCreate(rInt int, project string) string {
return fmt.Sprintf(`
resource "gitlab_pages_domain" "this" {
project = "%[2]s"
domain = "page-%[1]d.example.com"
}
`, rInt, project)
}

func testAccGitlabPagesDomainUpdate(rInt int, project string) string {
return fmt.Sprintf(`
resource "gitlab_pages_domain" "this" {
project = "%[2]s"
domain = "page-%[1]d.example.com"
}
`, rInt+1, project)
}