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

Add importer for pages projects #1886

Merged
merged 16 commits into from
Sep 20, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/1886.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/cloudflare_pages_project: Adds importer for pages_project
```
5 changes: 5 additions & 0 deletions docs/resources/pages_project.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,9 @@ Optional:
- `production_deployment_enabled` (Boolean) Enable production deployments.
- `repo_name` (String) Project repository name.

## Import

Import is supported using the following syntax:
```shell
$ terraform import cloudflare_pages_project.example <account_id>/<project_name>
```
1 change: 1 addition & 0 deletions examples/resources/cloudflare_pages_project/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$ terraform import cloudflare_pages_project.example <account_id>/<project_name>
115 changes: 115 additions & 0 deletions internal/provider/import_resource_cloudflare_pages_project_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package provider

import (
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func testPagesProjectFull(resourceID, accountID, projectName, repoOwner, repoName string) string {
return fmt.Sprintf(`
resource "cloudflare_pages_project" "%[1]s" {
account_id = "%[2]s"
name = "%[3]s"
production_branch = "main"
build_config {
build_command = "npm run build"
destination_dir = "build"
root_dir = "/"
web_analytics_tag = "cee1c73f6e4743d0b5e6bb1a0bcaabcc"
web_analytics_token = "021e1057c18547eca7b79f2516f06o7x"
}
source {
type = "github"
config {
owner = "%[4]s"
repo_name = "%[5]s"
production_branch = "main"
pr_comments_enabled = true
deployments_enabled = true
production_deployment_enabled = true
preview_deployment_setting = "custom"
preview_branch_includes = ["dev","preview"]
preview_branch_excludes = ["main", "prod"]

}
}
deployment_configs {
preview {
environment_variables = {
ENVIRONMENT = "preview"
}
kv_namespaces = {
KV_BINDING = "5eb63bbbe01eeed093cb22bb8f5acdc3"
}
durable_object_namespaces = {
DO_BINDING = "5eb63bbbe01eeed093cb22bb8f5acdc3"
}
r2_buckets = {
R2_BINDING = "some-bucket"
}
d1_databases = {
D1_BINDING = "445e2955-951a-4358-a35b-a4d0c813f63"
}
compatibility_date = "2022-08-15"
compatibility_flags = ["preview_flag"]
}
production {
environment_variables = {
ENVIRONMENT = "production"
OTHER_VALUE = "other value"
}
kv_namespaces = {
KV_BINDING_1 = "5eb63bbbe01eeed093cb22bb8f5acdc3"
KV_BINDING_2 = "3cdca5f8bb22bc390deee10ebbb36be5"
}
durable_object_namespaces = {
DO_BINDING_1 = "5eb63bbbe01eeed093cb22bb8f5acdc3"
DO_BINDING_2 = "3cdca5f8bb22bc390deee10ebbb36be5"
}
r2_buckets = {
R2_BINDING_1 = "some-bucket"
R2_BINDING_2 = "other-bucket"
}
d1_databases = {
D1_BINDING_1 = "445e2955-951a-4358-a35b-a4d0c813f63"
D1_BINDING_2 = "a399414b-c697-409a-a688-377db6433cd9"
}
compatibility_date = "2022-08-16"
compatibility_flags = ["production_flag", "second flag"]
}
}
}
`, resourceID, accountID, projectName, repoOwner, repoName)
}

func TestAccCloudflarePagesProject_Import(t *testing.T) {
skipPagesProjectForNonConfiguredDefaultAccount(t)

rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_pages_project.%s", rnd)
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
pagesOwner := os.Getenv("CLOUDFLARE_PAGES_OWNER")
pagesRepo := os.Getenv("CLOUDFLARE_PAGES_REPO")

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheckEmail(t)
testAccPreCheckApiKey(t)
},
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: testPagesProjectFull(rnd, accountID, rnd, pagesOwner, pagesRepo),
},
{
ResourceName: name,
ImportStateIdPrefix: fmt.Sprintf("%s/", accountID),
ImportState: true,
ImportStateVerify: true,
},
},
})
}
12 changes: 12 additions & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ func testAccPreCheckWorkspaceOne(t *testing.T) {
}

func testAccPreCheckPages(t *testing.T) {
testAccPreCheckAccount(t)

if v := os.Getenv("CLOUDFLARE_PAGES_OWNER"); v == "" {
t.Fatal("CLOUDFLARE_PAGES_OWNER must be set for this acceptance test")
}
Expand Down Expand Up @@ -199,3 +201,13 @@ func skipV1WAFTestForNonConfiguredDefaultZone(t *testing.T) {
t.Skipf("Skipping acceptance test as %s is using WAF v2 and cannot assert v1 resource configurations", testAccCloudflareZoneID)
}
}

// skipPagesProjectForNonConfiguredDefaultAccount ignores the pages project tests
// due to not having a dedicated GitHub account setup in Cloudflare for the
// default account. This will allow those who intentionally want to run the test
// to do so while keeping CI sane.
func skipPagesProjectForNonConfiguredDefaultAccount(t *testing.T) {
if os.Getenv("CLOUDFLARE_ACCOUNT_ID") == testAccCloudflareAccountID {
t.Skipf("Skipping acceptance test as %s is using pages project that isn't setup for CI", testAccCloudflareAccountID)
}
}
159 changes: 145 additions & 14 deletions internal/provider/resource_cloudflare_pages_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ package provider
import (
"context"
"fmt"
"reflect"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-log/tflog"

"github.com/MakeNowJust/heredoc/v2"
"github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
Expand All @@ -18,6 +22,9 @@ func resourceCloudflarePagesProject() *schema.Resource {
ReadContext: resourceCloudflarePagesProjectRead,
UpdateContext: resourceCloudflarePagesProjectUpdate,
DeleteContext: resourceCloudflarePagesProjectDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceCloudflarePagesProjectImport,
},
Description: heredoc.Doc(`
Provides a resource which manages Cloudflare Pages projects.
`),
Expand Down Expand Up @@ -83,6 +90,46 @@ func buildDeploymentConfig(environment interface{}) cloudflare.PagesProjectDeplo
return config
}

func parseDeployementConfig(deployment cloudflare.PagesProjectDeploymentConfigEnvironment) (returnValue []map[string]interface{}) {
config := make(map[string]interface{})

config["compatibility_date"] = deployment.CompatibilityDate
config["compatibility_flags"] = deployment.CompatibilityFlags

deploymentVars := map[string]string{}
for key, value := range deployment.EnvVars {
deploymentVars[key] = value.Value
}
config["environment_variables"] = deploymentVars

deploymentVars = map[string]string{}
for key, value := range deployment.KvNamespaces {
deploymentVars[key] = value.Value
}
config["kv_namespaces"] = deploymentVars

deploymentVars = map[string]string{}
for key, value := range deployment.DoNamespaces {
deploymentVars[key] = value.Value
}
config["durable_object_namespaces"] = deploymentVars

deploymentVars = map[string]string{}
for key, value := range deployment.R2Bindings {
deploymentVars[key] = value.Name
}
config["r2_buckets"] = deploymentVars

deploymentVars = map[string]string{}
for key, value := range deployment.D1Databases {
deploymentVars[key] = value.ID
}
config["d1_databases"] = deploymentVars

returnValue = append(returnValue, config)
return
}

func buildPagesProject(d *schema.ResourceData) cloudflare.PagesProject {
project := cloudflare.PagesProject{}
project.Name = d.Get("name").(string)
Expand Down Expand Up @@ -133,6 +180,9 @@ func buildPagesProject(d *schema.ResourceData) cloudflare.PagesProject {
if productionBranch, ok := d.GetOk("source.0.config.0.production_branch"); ok {
sourceConfig.ProductionBranch = productionBranch.(string)
}
if productionBranchEnable, ok := d.GetOk("source.0.config.0.production_deployment_enabled"); ok {
sourceConfig.ProductionDeploymentsEnabled = productionBranchEnable.(bool)
}
if previewBranchIncludes, ok := d.GetOk("source.0.config.0.preview_branch_includes"); ok {
for _, item := range previewBranchIncludes.([]interface{}) {
sourceConfig.PreviewBranchIncludes = append(sourceConfig.PreviewBranchIncludes, item.(string))
Expand Down Expand Up @@ -164,17 +214,69 @@ func buildPagesProject(d *schema.ResourceData) cloudflare.PagesProject {
func resourceCloudflarePagesProjectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
accountID := d.Get("account_id").(string)
projectName := d.Get("name").(string)

res, err := client.PagesProject(ctx, accountID, projectName)
project, err := client.PagesProject(ctx, accountID, d.Id())
if err != nil {
return diag.FromErr(fmt.Errorf("error reading cloudflare pages project %q: %w", projectName, err))
return diag.FromErr(fmt.Errorf("error reading cloudflare pages project %q: %w", d.Id(), err))
}

tflog.Debug(ctx, fmt.Sprintf("Cloudflare Pages Project Response: %#v", project))
d.Set("subdomain", project.SubDomain)
d.Set("production_branch", project.ProductionBranch)
d.Set("account_id", accountID)
d.Set("domains", project.Domains)
d.Set("created_on", project.CreatedOn.Format(time.RFC3339))

if project.Source != nil {
source := []map[string]interface{}{}
source = append(source, map[string]interface{}{
"type": project.Source.Type,
"config": []map[string]interface{}{
{
"owner": project.Source.Config.Owner,
"repo_name": project.Source.Config.RepoName,
"production_branch": project.Source.Config.ProductionBranch,
"pr_comments_enabled": project.Source.Config.PRCommentsEnabled,
"deployments_enabled": project.Source.Config.DeploymentsEnabled,
"production_deployment_enabled": project.Source.Config.ProductionDeploymentsEnabled,
"preview_branch_includes": project.Source.Config.PreviewBranchIncludes,
"preview_branch_excludes": project.Source.Config.PreviewBranchExcludes,
"preview_deployment_setting": project.Source.Config.PreviewDeploymentSetting,
},
},
},
)
d.Set("source", source)
}
emptyProjectBuildConfig := cloudflare.PagesProjectBuildConfig{}
if project.BuildConfig != emptyProjectBuildConfig {
buildConfig := []map[string]interface{}{}
buildConfig = append(buildConfig, map[string]interface{}{
"build_command": project.BuildConfig.BuildCommand,
"destination_dir": project.BuildConfig.DestinationDir,
"root_dir": project.BuildConfig.RootDir,
"web_analytics_tag": project.BuildConfig.WebAnalyticsTag,
"web_analytics_token": project.BuildConfig.WebAnalyticsToken,
},
)
d.Set("build_config", buildConfig)
}

d.SetId(res.ID)
Cyb3r-Jak3 marked this conversation as resolved.
Show resolved Hide resolved
d.Set("subdomain", res.SubDomain)
d.Set("created_on", res.CreatedOn.Format(time.RFC3339))
d.Set("domains", res.Domains)
emptyDeploymentConfig := cloudflare.PagesProjectDeploymentConfigs{}
if !reflect.DeepEqual(project.DeploymentConfigs, emptyDeploymentConfig) {
deploymentConfigs := []map[string]interface{}{}
deploymentConfig := make(map[string]interface{})
emptyDeploymentEnviroment := cloudflare.PagesProjectDeploymentConfigEnvironment{}
if !reflect.DeepEqual(project.DeploymentConfigs.Preview, emptyDeploymentEnviroment) {
deploymentConfig["preview"] = parseDeployementConfig(project.DeploymentConfigs.Preview)
}

if !reflect.DeepEqual(project.DeploymentConfigs.Production, emptyDeploymentEnviroment) {
deploymentConfig["production"] = parseDeployementConfig(project.DeploymentConfigs.Production)
}
deploymentConfigs = append(deploymentConfigs, deploymentConfig)
d.Set("deployment_configs", deploymentConfigs)
}

return nil
}
Expand All @@ -184,24 +286,24 @@ func resourceCloudflarePagesProjectCreate(ctx context.Context, d *schema.Resourc
accountID := d.Get("account_id").(string)
pageProject := buildPagesProject(d)

_, err := client.CreatePagesProject(ctx, accountID, pageProject)
project, err := client.CreatePagesProject(ctx, accountID, pageProject)
if err != nil {
return diag.FromErr(fmt.Errorf("error creating cloudflare pages project %q: %w", pageProject.Name, err))
}

d.SetId(project.Name)
return resourceCloudflarePagesProjectRead(ctx, d, meta)
}

func resourceCloudflarePagesProjectUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
accountID := d.Get("account_id").(string)
projectName := d.Get("name").(string)

pageProject := buildPagesProject(d)

_, err := client.UpdatePagesProject(ctx, accountID, projectName, pageProject)
_, err := client.UpdatePagesProject(ctx, accountID, d.Id(), pageProject)
if err != nil {
return diag.FromErr(fmt.Errorf("error updating cloudflare pages project %q: %w", pageProject.Name, err))
return diag.FromErr(fmt.Errorf("error updating cloudflare pages project %q: %w", d.Id(), err))
}

return resourceCloudflarePagesProjectRead(ctx, d, meta)
Expand All @@ -210,11 +312,40 @@ func resourceCloudflarePagesProjectUpdate(ctx context.Context, d *schema.Resourc
func resourceCloudflarePagesProjectDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
accountID := d.Get("account_id").(string)
projectName := d.Get("name").(string)

err := client.DeletePagesProject(ctx, accountID, projectName)
err := client.DeletePagesProject(ctx, accountID, d.Id())
if err != nil {
return diag.FromErr(fmt.Errorf("error deleting cloudflare pages project %q: %w", projectName, err))
return diag.FromErr(fmt.Errorf("error deleting cloudflare pages project %q: %w", d.Id(), err))
}
return nil
}

func resourceCloudflarePagesProjectImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
client := meta.(*cloudflare.API)

// split the id so we can look up
idAttr := strings.SplitN(d.Id(), "/", 2)
var accountID string
var projectName string
if len(idAttr) == 2 {
accountID = idAttr[0]
projectName = idAttr[1]
} else {
return nil, fmt.Errorf("invalid id %q specified, should be in format \"accountID/project-name\" for import", d.Id())
}

project, err := client.PagesProject(ctx, accountID, projectName)
if err != nil {
return nil, fmt.Errorf("Unable to find record with ID %q: %w", d.Id(), err)
}

tflog.Info(ctx, fmt.Sprintf("Found project: %s", project.Name))

d.SetId(project.Name)
Cyb3r-Jak3 marked this conversation as resolved.
Show resolved Hide resolved
d.Set("name", project.Name)
d.Set("account_id", accountID)

resourceCloudflarePagesProjectRead(ctx, d, meta)

return []*schema.ResourceData{d}, nil
}