Skip to content

Commit b702889

Browse files
committed
feat: Add custom_properties to github_repository and github_enterprise_custom_property resource (#3230, #3304)
Allow custom properties to be set on repositories at creation time, fixing 422 errors when an organization enforces required custom properties. Adds a new github_enterprise_custom_property resource and data source for managing custom property definitions at the enterprise level. Uses context-aware CRUD functions, proper 404 handling, and ConfigStateChecks in acceptance tests per maintainer guidelines. Closes #3230, #3304
1 parent 866a673 commit b702889

8 files changed

+750
-0
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package github
2+
3+
import (
4+
"context"
5+
6+
"github.com/google/go-github/v84/github"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
)
10+
11+
func dataSourceGithubEnterpriseCustomProperty() *schema.Resource {
12+
return &schema.Resource{
13+
Description: "Use this data source to retrieve information about a custom property definition for a GitHub enterprise.",
14+
15+
ReadContext: dataSourceGithubEnterpriseCustomPropertyRead,
16+
17+
Schema: map[string]*schema.Schema{
18+
"enterprise_slug": {
19+
Type: schema.TypeString,
20+
Required: true,
21+
Description: "The slug of the enterprise.",
22+
},
23+
"property_name": {
24+
Type: schema.TypeString,
25+
Required: true,
26+
Description: "The name of the custom property.",
27+
},
28+
"value_type": {
29+
Type: schema.TypeString,
30+
Computed: true,
31+
Description: "The type of the value for the property.",
32+
},
33+
"required": {
34+
Type: schema.TypeBool,
35+
Computed: true,
36+
Description: "Whether the custom property is required.",
37+
},
38+
"default_values": {
39+
Type: schema.TypeList,
40+
Computed: true,
41+
Description: "The default value(s) of the custom property.",
42+
Elem: &schema.Schema{Type: schema.TypeString},
43+
},
44+
"description": {
45+
Type: schema.TypeString,
46+
Computed: true,
47+
Description: "A short description of the custom property.",
48+
},
49+
"allowed_values": {
50+
Type: schema.TypeList,
51+
Computed: true,
52+
Description: "An ordered list of allowed values for the property.",
53+
Elem: &schema.Schema{Type: schema.TypeString},
54+
},
55+
"values_editable_by": {
56+
Type: schema.TypeString,
57+
Computed: true,
58+
Description: "Who can edit the values of the property. Can be one of 'org_actors' or 'org_and_repo_actors'.",
59+
},
60+
},
61+
}
62+
}
63+
64+
func dataSourceGithubEnterpriseCustomPropertyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
65+
client := meta.(*Owner).v3client
66+
67+
enterpriseSlug := d.Get("enterprise_slug").(string)
68+
propertyName := d.Get("property_name").(string)
69+
70+
property, _, err := client.Enterprise.GetCustomProperty(ctx, enterpriseSlug, propertyName)
71+
if err != nil {
72+
return diag.Errorf("error reading enterprise custom property %s/%s: %v", enterpriseSlug, propertyName, err)
73+
}
74+
75+
var defaultValues []string
76+
if property.ValueType == github.PropertyValueTypeMultiSelect {
77+
if vals, ok := property.DefaultValueStrings(); ok {
78+
defaultValues = vals
79+
}
80+
} else {
81+
if val, ok := property.DefaultValueString(); ok {
82+
defaultValues = []string{val}
83+
}
84+
}
85+
86+
d.SetId(buildTwoPartID(enterpriseSlug, propertyName))
87+
88+
if err := d.Set("enterprise_slug", enterpriseSlug); err != nil {
89+
return diag.FromErr(err)
90+
}
91+
if err := d.Set("property_name", property.GetPropertyName()); err != nil {
92+
return diag.FromErr(err)
93+
}
94+
if err := d.Set("value_type", string(property.ValueType)); err != nil {
95+
return diag.FromErr(err)
96+
}
97+
if err := d.Set("required", property.GetRequired()); err != nil {
98+
return diag.FromErr(err)
99+
}
100+
if err := d.Set("default_values", defaultValues); err != nil {
101+
return diag.FromErr(err)
102+
}
103+
if err := d.Set("description", property.GetDescription()); err != nil {
104+
return diag.FromErr(err)
105+
}
106+
if err := d.Set("allowed_values", property.AllowedValues); err != nil {
107+
return diag.FromErr(err)
108+
}
109+
if err := d.Set("values_editable_by", property.GetValuesEditableBy()); err != nil {
110+
return diag.FromErr(err)
111+
}
112+
113+
return nil
114+
}

github/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ func Provider() *schema.Provider {
218218
"github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(),
219219
"github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(),
220220
"github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(),
221+
"github_enterprise_custom_property": resourceGithubEnterpriseCustomProperties(),
221222
"github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(),
222223
},
223224

@@ -295,6 +296,7 @@ func Provider() *schema.Provider {
295296
"github_user_external_identity": dataSourceGithubUserExternalIdentity(),
296297
"github_users": dataSourceGithubUsers(),
297298
"github_enterprise": dataSourceGithubEnterprise(),
299+
"github_enterprise_custom_property": dataSourceGithubEnterpriseCustomProperty(),
298300
"github_repository_environment_deployment_policies": dataSourceGithubRepositoryEnvironmentDeploymentPolicies(),
299301
},
300302
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"log"
6+
"net/http"
7+
8+
"github.com/google/go-github/v84/github"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
12+
)
13+
14+
func resourceGithubEnterpriseCustomProperties() *schema.Resource {
15+
return &schema.Resource{
16+
CreateContext: resourceGithubEnterpriseCustomPropertiesCreate,
17+
ReadContext: resourceGithubEnterpriseCustomPropertiesRead,
18+
UpdateContext: resourceGithubEnterpriseCustomPropertiesUpdate,
19+
DeleteContext: resourceGithubEnterpriseCustomPropertiesDelete,
20+
Importer: &schema.ResourceImporter{
21+
StateContext: schema.ImportStatePassthroughContext,
22+
},
23+
24+
Schema: map[string]*schema.Schema{
25+
"enterprise_slug": {
26+
Type: schema.TypeString,
27+
Required: true,
28+
ForceNew: true,
29+
Description: "The slug of the enterprise.",
30+
},
31+
"property_name": {
32+
Type: schema.TypeString,
33+
Required: true,
34+
ForceNew: true,
35+
Description: "The name of the custom property.",
36+
},
37+
"value_type": {
38+
Type: schema.TypeString,
39+
Required: true,
40+
Description: "The type of the value for the property. Can be one of: 'string', 'single_select', 'multi_select', 'true_false', 'url'.",
41+
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{string(github.PropertyValueTypeString), string(github.PropertyValueTypeSingleSelect), string(github.PropertyValueTypeMultiSelect), string(github.PropertyValueTypeTrueFalse), string(github.PropertyValueTypeURL)}, false)),
42+
},
43+
"required": {
44+
Type: schema.TypeBool,
45+
Optional: true,
46+
Description: "Whether the custom property is required.",
47+
},
48+
"default_values": {
49+
Type: schema.TypeList,
50+
Optional: true,
51+
Computed: true,
52+
Description: "The default value(s) of the custom property. For 'multi_select' properties, multiple values may be specified. For all other types, provide a single value.",
53+
Elem: &schema.Schema{Type: schema.TypeString},
54+
},
55+
"description": {
56+
Type: schema.TypeString,
57+
Optional: true,
58+
Description: "A short description of the custom property.",
59+
},
60+
"allowed_values": {
61+
Type: schema.TypeList,
62+
Optional: true,
63+
Computed: true,
64+
Description: "An ordered list of allowed values for the property. Only applicable to 'single_select' and 'multi_select' types.",
65+
Elem: &schema.Schema{Type: schema.TypeString},
66+
},
67+
"values_editable_by": {
68+
Type: schema.TypeString,
69+
Optional: true,
70+
Computed: true,
71+
Description: "Who can edit the values of the property. Can be one of: 'org_actors', 'org_and_repo_actors'. Defaults to 'org_actors'.",
72+
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"org_actors", "org_and_repo_actors"}, false)),
73+
},
74+
},
75+
}
76+
}
77+
78+
func resourceGithubEnterpriseCustomPropertiesCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
79+
client := meta.(*Owner).v3client
80+
81+
enterpriseSlug := d.Get("enterprise_slug").(string)
82+
propertyName := d.Get("property_name").(string)
83+
84+
property := buildEnterpriseCustomProperty(d)
85+
86+
_, _, err := client.Enterprise.CreateOrUpdateCustomProperty(ctx, enterpriseSlug, propertyName, property)
87+
if err != nil {
88+
return diag.FromErr(err)
89+
}
90+
91+
d.SetId(buildTwoPartID(enterpriseSlug, propertyName))
92+
return resourceGithubEnterpriseCustomPropertiesRead(ctx, d, meta)
93+
}
94+
95+
func resourceGithubEnterpriseCustomPropertiesRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
96+
client := meta.(*Owner).v3client
97+
98+
enterpriseSlug, propertyName, err := parseTwoPartID(d.Id(), "enterprise_slug", "property_name")
99+
if err != nil {
100+
return diag.FromErr(err)
101+
}
102+
103+
property, resp, err := client.Enterprise.GetCustomProperty(ctx, enterpriseSlug, propertyName)
104+
if err != nil {
105+
if resp != nil && resp.StatusCode == http.StatusNotFound {
106+
log.Printf("[INFO] Removing enterprise custom property %s/%s from state because it no longer exists", enterpriseSlug, propertyName)
107+
d.SetId("")
108+
return nil
109+
}
110+
return diag.FromErr(err)
111+
}
112+
113+
if err := d.Set("enterprise_slug", enterpriseSlug); err != nil {
114+
return diag.FromErr(err)
115+
}
116+
if err := d.Set("property_name", property.GetPropertyName()); err != nil {
117+
return diag.FromErr(err)
118+
}
119+
if err := d.Set("value_type", string(property.ValueType)); err != nil {
120+
return diag.FromErr(err)
121+
}
122+
if err := d.Set("required", property.GetRequired()); err != nil {
123+
return diag.FromErr(err)
124+
}
125+
126+
var defaultValues []string
127+
if property.ValueType == github.PropertyValueTypeMultiSelect {
128+
if vals, ok := property.DefaultValueStrings(); ok {
129+
defaultValues = vals
130+
}
131+
} else {
132+
if val, ok := property.DefaultValueString(); ok {
133+
defaultValues = []string{val}
134+
}
135+
}
136+
if err := d.Set("default_values", defaultValues); err != nil {
137+
return diag.FromErr(err)
138+
}
139+
if err := d.Set("description", property.GetDescription()); err != nil {
140+
return diag.FromErr(err)
141+
}
142+
if err := d.Set("allowed_values", property.AllowedValues); err != nil {
143+
return diag.FromErr(err)
144+
}
145+
if err := d.Set("values_editable_by", property.GetValuesEditableBy()); err != nil {
146+
return diag.FromErr(err)
147+
}
148+
149+
return nil
150+
}
151+
152+
func resourceGithubEnterpriseCustomPropertiesUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
153+
client := meta.(*Owner).v3client
154+
155+
enterpriseSlug, propertyName, err := parseTwoPartID(d.Id(), "enterprise_slug", "property_name")
156+
if err != nil {
157+
return diag.FromErr(err)
158+
}
159+
160+
property := buildEnterpriseCustomProperty(d)
161+
162+
_, _, err = client.Enterprise.CreateOrUpdateCustomProperty(ctx, enterpriseSlug, propertyName, property)
163+
if err != nil {
164+
return diag.FromErr(err)
165+
}
166+
167+
return resourceGithubEnterpriseCustomPropertiesRead(ctx, d, meta)
168+
}
169+
170+
func resourceGithubEnterpriseCustomPropertiesDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
171+
client := meta.(*Owner).v3client
172+
173+
enterpriseSlug, propertyName, err := parseTwoPartID(d.Id(), "enterprise_slug", "property_name")
174+
if err != nil {
175+
return diag.FromErr(err)
176+
}
177+
178+
resp, err := client.Enterprise.RemoveCustomProperty(ctx, enterpriseSlug, propertyName)
179+
if err != nil {
180+
if resp != nil && resp.StatusCode == http.StatusNotFound {
181+
return nil
182+
}
183+
return diag.FromErr(err)
184+
}
185+
186+
return nil
187+
}
188+
189+
func resourceGithubEnterpriseCustomPropertiesImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
190+
enterpriseSlug, propertyName, err := parseTwoPartID(d.Id(), "enterprise_slug", "property_name")
191+
if err != nil {
192+
return nil, err
193+
}
194+
195+
if err := d.Set("enterprise_slug", enterpriseSlug); err != nil {
196+
return nil, err
197+
}
198+
if err := d.Set("property_name", propertyName); err != nil {
199+
return nil, err
200+
}
201+
202+
return []*schema.ResourceData{d}, nil
203+
}
204+
205+
func buildEnterpriseCustomProperty(d *schema.ResourceData) *github.CustomProperty {
206+
propertyName := d.Get("property_name").(string)
207+
valueType := github.PropertyValueType(d.Get("value_type").(string))
208+
required := d.Get("required").(bool)
209+
description := d.Get("description").(string)
210+
211+
rawAllowedValues := d.Get("allowed_values").([]any)
212+
allowedValues := make([]string, 0, len(rawAllowedValues))
213+
for _, v := range rawAllowedValues {
214+
allowedValues = append(allowedValues, v.(string))
215+
}
216+
217+
property := &github.CustomProperty{
218+
PropertyName: &propertyName,
219+
ValueType: valueType,
220+
Required: &required,
221+
Description: &description,
222+
AllowedValues: allowedValues,
223+
}
224+
225+
rawDefaultValues := d.Get("default_values").([]any)
226+
defaultValues := make([]string, 0, len(rawDefaultValues))
227+
for _, v := range rawDefaultValues {
228+
defaultValues = append(defaultValues, v.(string))
229+
}
230+
if len(defaultValues) > 0 {
231+
if valueType == github.PropertyValueTypeMultiSelect {
232+
property.DefaultValue = defaultValues
233+
} else {
234+
property.DefaultValue = defaultValues[0]
235+
}
236+
}
237+
238+
if val, ok := d.GetOk("values_editable_by"); ok {
239+
str := val.(string)
240+
property.ValuesEditableBy = &str
241+
}
242+
243+
return property
244+
}

0 commit comments

Comments
 (0)