Skip to content

Commit fa01ee8

Browse files
committed
fix: improve github_organization_custom_properties resource
- Remove broken CustomizeDiff referencing non-existent 'slug' field - Make 'property_name' ForceNew (renaming creates a new property, orphans old) - Make 'value_type' Required and ForceNew (can't create without type, changing type requires recreation) - Add allowed_values validation: required for select types, rejected for others - Add checkOrganization guard to all CRUD functions - Add graceful 404 handling in Read (removes from state instead of erroring) - Improve error messages with context - Fix Update double-read (Create already calls Read at the end) - Add resource Description - Use GetOk for optional fields to avoid sending zero values Resolves #2806
1 parent 866a673 commit fa01ee8

2 files changed

Lines changed: 107 additions & 35 deletions

File tree

github/resource_github_organization_custom_properties.go

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,38 @@ package github
22

33
import (
44
"context"
5+
"fmt"
6+
"log"
7+
"net/http"
58

69
"github.com/google/go-github/v84/github"
7-
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
810
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
911
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1012
)
1113

1214
func resourceGithubOrganizationCustomProperties() *schema.Resource {
1315
return &schema.Resource{
14-
Create: resourceGithubCustomPropertiesCreate,
15-
Read: resourceGithubCustomPropertiesRead,
16-
Update: resourceGithubCustomPropertiesUpdate,
17-
Delete: resourceGithubCustomPropertiesDelete,
16+
Description: "Creates and manages a custom property for a GitHub Organization.",
17+
Create: resourceGithubCustomPropertiesCreate,
18+
Read: resourceGithubCustomPropertiesRead,
19+
Update: resourceGithubCustomPropertiesUpdate,
20+
Delete: resourceGithubCustomPropertiesDelete,
1821
Importer: &schema.ResourceImporter{
1922
State: resourceGithubCustomPropertiesImport,
2023
},
2124

22-
CustomizeDiff: customdiff.Sequence(
23-
customdiff.ComputedIf("slug", func(_ context.Context, d *schema.ResourceDiff, meta any) bool {
24-
return d.HasChange("name")
25-
}),
26-
),
27-
2825
Schema: map[string]*schema.Schema{
2926
"property_name": {
3027
Type: schema.TypeString,
3128
Required: true,
29+
ForceNew: true,
3230
Description: "The name of the custom property",
3331
},
3432
"value_type": {
3533
Type: schema.TypeString,
36-
Optional: true,
37-
Description: "The type of the custom property",
34+
Required: true,
35+
ForceNew: true,
36+
Description: "The type of the custom property. Can be one of: 'string', 'single_select', 'multi_select', 'true_false', or 'url'.",
3837
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{string(github.PropertyValueTypeString), string(github.PropertyValueTypeSingleSelect), string(github.PropertyValueTypeMultiSelect), string(github.PropertyValueTypeTrueFalse), string(github.PropertyValueTypeURL)}, false)),
3938
},
4039
"required": {
@@ -73,52 +72,83 @@ func resourceGithubOrganizationCustomProperties() *schema.Resource {
7372
}
7473

7574
func resourceGithubCustomPropertiesCreate(d *schema.ResourceData, meta any) error {
75+
if err := checkOrganization(meta); err != nil {
76+
return err
77+
}
78+
7679
ctx := context.Background()
7780
client := meta.(*Owner).v3client
7881
ownerName := meta.(*Owner).name
7982

8083
propertyName := d.Get("property_name").(string)
8184
valueType := github.PropertyValueType(d.Get("value_type").(string))
8285
required := d.Get("required").(bool)
83-
defaultValue := d.Get("default_value").(string)
8486
description := d.Get("description").(string)
85-
allowedValues := d.Get("allowed_values").([]any)
86-
var allowedValuesString []string
87-
for _, v := range allowedValues {
88-
allowedValuesString = append(allowedValuesString, v.(string))
89-
}
9087

9188
customProperty := &github.CustomProperty{
92-
PropertyName: &propertyName,
93-
ValueType: valueType,
94-
Required: &required,
95-
DefaultValue: &defaultValue,
96-
Description: &description,
97-
AllowedValues: allowedValuesString,
89+
PropertyName: &propertyName,
90+
ValueType: valueType,
91+
Required: &required,
92+
Description: &description,
93+
}
94+
95+
// Set default value if provided
96+
if v, ok := d.GetOk("default_value"); ok {
97+
defaultValue := v.(string)
98+
customProperty.DefaultValue = &defaultValue
99+
}
100+
101+
// Set allowed values if provided (only valid for select types)
102+
if v, ok := d.GetOk("allowed_values"); ok {
103+
allowedValues := expandStringList(v.([]any))
104+
if valueType == github.PropertyValueTypeSingleSelect || valueType == github.PropertyValueTypeMultiSelect {
105+
customProperty.AllowedValues = allowedValues
106+
} else {
107+
return fmt.Errorf("allowed_values can only be set for single_select or multi_select value types")
108+
}
109+
}
110+
111+
// Validate that allowed_values is provided for select types
112+
if (valueType == github.PropertyValueTypeSingleSelect || valueType == github.PropertyValueTypeMultiSelect) && len(customProperty.AllowedValues) == 0 {
113+
return fmt.Errorf("allowed_values is required for %s value type", valueType)
98114
}
99115

100116
if val, ok := d.GetOk("values_editable_by"); ok {
101117
str := val.(string)
102118
customProperty.ValuesEditableBy = &str
103119
}
104120

105-
customProperty, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, ownerName, d.Get("property_name").(string), customProperty)
121+
customProperty, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, ownerName, propertyName, customProperty)
106122
if err != nil {
107-
return err
123+
return fmt.Errorf("error creating organization custom property %s: %w", propertyName, err)
108124
}
109125

110126
d.SetId(*customProperty.PropertyName)
111127
return resourceGithubCustomPropertiesRead(d, meta)
112128
}
113129

114130
func resourceGithubCustomPropertiesRead(d *schema.ResourceData, meta any) error {
131+
if err := checkOrganization(meta); err != nil {
132+
return err
133+
}
134+
115135
ctx := context.Background()
116136
client := meta.(*Owner).v3client
117137
ownerName := meta.(*Owner).name
118138

119-
customProperty, _, err := client.Organizations.GetCustomProperty(ctx, ownerName, d.Get("property_name").(string))
139+
propertyName := d.Id()
140+
if pn, ok := d.GetOk("property_name"); ok {
141+
propertyName = pn.(string)
142+
}
143+
144+
customProperty, resp, err := client.Organizations.GetCustomProperty(ctx, ownerName, propertyName)
120145
if err != nil {
121-
return err
146+
if resp != nil && resp.StatusCode == http.StatusNotFound {
147+
log.Printf("[WARN] Removing organization custom property %s from state because it no longer exists in GitHub", propertyName)
148+
d.SetId("")
149+
return nil
150+
}
151+
return fmt.Errorf("error reading organization custom property %s: %w", propertyName, err)
122152
}
123153

124154
// TODO: Add support for other types of default values
@@ -137,19 +167,22 @@ func resourceGithubCustomPropertiesRead(d *schema.ResourceData, meta any) error
137167
}
138168

139169
func resourceGithubCustomPropertiesUpdate(d *schema.ResourceData, meta any) error {
140-
if err := resourceGithubCustomPropertiesCreate(d, meta); err != nil {
141-
return err
142-
}
143-
return resourceGithubCustomPropertiesRead(d, meta)
170+
// Create uses the same upsert API, and already calls Read at the end
171+
return resourceGithubCustomPropertiesCreate(d, meta)
144172
}
145173

146174
func resourceGithubCustomPropertiesDelete(d *schema.ResourceData, meta any) error {
175+
if err := checkOrganization(meta); err != nil {
176+
return err
177+
}
178+
147179
client := meta.(*Owner).v3client
148180
ownerName := meta.(*Owner).name
181+
propertyName := d.Get("property_name").(string)
149182

150-
_, err := client.Organizations.RemoveCustomProperty(context.Background(), ownerName, d.Get("property_name").(string))
183+
_, err := client.Organizations.RemoveCustomProperty(context.Background(), ownerName, propertyName)
151184
if err != nil {
152-
return err
185+
return fmt.Errorf("error deleting organization custom property %s: %w", propertyName, err)
153186
}
154187

155188
return nil

github/resource_github_organization_custom_properties_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,45 @@ func TestAccGithubOrganizationCustomPropertiesValidation(t *testing.T) {
3030
},
3131
})
3232
})
33+
34+
t.Run("rejects allowed_values for string type", func(t *testing.T) {
35+
config := `
36+
resource "github_organization_custom_properties" "test" {
37+
property_name = "TestInvalidAllowedValues"
38+
value_type = "string"
39+
allowed_values = ["a", "b"]
40+
}`
41+
42+
resource.Test(t, resource.TestCase{
43+
PreCheck: func() { skipUnlessHasOrgs(t) },
44+
ProviderFactories: providerFactories,
45+
Steps: []resource.TestStep{
46+
{
47+
Config: config,
48+
ExpectError: regexp.MustCompile("allowed_values can only be set for single_select or multi_select"),
49+
},
50+
},
51+
})
52+
})
53+
54+
t.Run("requires allowed_values for single_select type", func(t *testing.T) {
55+
config := `
56+
resource "github_organization_custom_properties" "test" {
57+
property_name = "TestMissingAllowedValues"
58+
value_type = "single_select"
59+
}`
60+
61+
resource.Test(t, resource.TestCase{
62+
PreCheck: func() { skipUnlessHasOrgs(t) },
63+
ProviderFactories: providerFactories,
64+
Steps: []resource.TestStep{
65+
{
66+
Config: config,
67+
ExpectError: regexp.MustCompile("allowed_values is required for single_select"),
68+
},
69+
},
70+
})
71+
})
3372
}
3473

3574
func TestAccGithubOrganizationCustomProperties(t *testing.T) {

0 commit comments

Comments
 (0)