55 "fmt"
66
77 unleash "github.com/Unleash/unleash-server-api-go/client"
8+ "github.com/hashicorp/terraform-plugin-framework/diag"
89 "github.com/hashicorp/terraform-plugin-framework/path"
910 "github.com/hashicorp/terraform-plugin-framework/resource"
1011 "github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -27,10 +28,23 @@ type projectResource struct {
2728}
2829
2930type projectResourceModel struct {
30- Id types.String `tfsdk:"id"`
31- Name types.String `tfsdk:"name"`
31+ Id types.String `tfsdk:"id"`
32+ Name types.String `tfsdk:"name"`
33+ Description types.String `tfsdk:"description"`
34+ Mode types.String `tfsdk:"mode"`
35+ FeatureNaming * featureNamingModel `tfsdk:"feature_naming"`
36+ LinkTemplates []projectLinkTemplateModel `tfsdk:"link_templates"`
37+ }
38+
39+ type featureNamingModel struct {
40+ Pattern types.String `tfsdk:"pattern"`
41+ Example types.String `tfsdk:"example"`
3242 Description types.String `tfsdk:"description"`
33- Mode types.String `tfsdk:"mode"`
43+ }
44+
45+ type projectLinkTemplateModel struct {
46+ Title types.String `tfsdk:"title"`
47+ UrlTemplate types.String `tfsdk:"url_template"`
3448}
3549
3650// Configure adds the provider configured client to the data source.
@@ -77,6 +91,40 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re
7791 Computed : true ,
7892 Optional : true ,
7993 },
94+ "feature_naming" : schema.SingleNestedAttribute {
95+ Description : "Optional feature naming pattern applied to all features created in this project." ,
96+ Optional : true ,
97+ Attributes : map [string ]schema.Attribute {
98+ "pattern" : schema.StringAttribute {
99+ Description : "A JavaScript regular expression pattern, without the start and end delimiters." ,
100+ Required : true ,
101+ },
102+ "example" : schema.StringAttribute {
103+ Description : "An example feature name that matches the pattern." ,
104+ Optional : true ,
105+ },
106+ "description" : schema.StringAttribute {
107+ Description : "A human-readable description of the pattern." ,
108+ Optional : true ,
109+ },
110+ },
111+ },
112+ "link_templates" : schema.ListNestedAttribute {
113+ Description : "Optional list of link templates automatically added to new feature flags." ,
114+ Optional : true ,
115+ NestedObject : schema.NestedAttributeObject {
116+ Attributes : map [string ]schema.Attribute {
117+ "title" : schema.StringAttribute {
118+ Description : "Link title shown in the Unleash UI." ,
119+ Optional : true ,
120+ },
121+ "url_template" : schema.StringAttribute {
122+ Description : "URL template that can contain {{project}} or {{feature}} placeholders." ,
123+ Required : true ,
124+ },
125+ },
126+ },
127+ },
80128 },
81129 }
82130}
@@ -122,6 +170,22 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest
122170 updateProjectSettingsRequest := * unleash .NewUpdateProjectEnterpriseSettingsSchemaWithDefaults ()
123171 updateProjectSettingsRequest .SetMode (mode )
124172
173+ featureNaming := expandFeatureNaming (plan .FeatureNaming , & resp .Diagnostics )
174+ if resp .Diagnostics .HasError () {
175+ return
176+ }
177+ if featureNaming != nil {
178+ updateProjectSettingsRequest .SetFeatureNaming (* featureNaming )
179+ }
180+
181+ linkTemplates := expandLinkTemplates (plan .LinkTemplates , & resp .Diagnostics )
182+ if resp .Diagnostics .HasError () {
183+ return
184+ }
185+ if linkTemplates != nil {
186+ updateProjectSettingsRequest .SetLinkTemplates (linkTemplates )
187+ }
188+
125189 updateSettingsResponse , err := r .client .ProjectsAPI .UpdateProjectEnterpriseSettings (ctx , * plan .Id .ValueStringPointer ()).UpdateProjectEnterpriseSettingsSchema (updateProjectSettingsRequest ).Execute ()
126190
127191 if ! ValidateApiResponse (updateSettingsResponse , 200 , & resp .Diagnostics , err ) {
@@ -189,20 +253,29 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re
189253
190254func (r * projectResource ) Update (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) {
191255 tflog .Debug (ctx , "Preparing to update project resource" )
192- var state projectResourceModel
193- resp .Diagnostics .Append (req .Plan .Get (ctx , & state )... )
256+ var plan projectResourceModel
257+ resp .Diagnostics .Append (req .Plan .Get (ctx , & plan )... )
194258
195259 if resp .Diagnostics .HasError () {
196260 return
197261 }
198262
199263 updateProjectSchema := * unleash .NewUpdateProjectSchemaWithDefaults ()
200- updateProjectSchema .Name = * state .Name .ValueStringPointer ()
201- if ! state .Description .IsNull () {
202- updateProjectSchema .Description = state .Description .ValueStringPointer ()
264+ updateProjectSchema .Name = * plan .Name .ValueStringPointer ()
265+ if ! plan .Description .IsNull () {
266+ updateProjectSchema .Description = plan .Description .ValueStringPointer ()
203267 }
204268
205- mode , err := resolveRequestedMode (state )
269+ if plan .Id .IsNull () || plan .Id .IsUnknown () {
270+ var state projectResourceModel
271+ resp .Diagnostics .Append (req .State .Get (ctx , & state )... )
272+ if resp .Diagnostics .HasError () {
273+ return
274+ }
275+ plan .Id = state .Id
276+ }
277+
278+ mode , err := resolveRequestedMode (plan )
206279 if err != nil {
207280 resp .Diagnostics .AddError (err .Error (), "InvalidMode" )
208281 return
@@ -211,15 +284,29 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest
211284 updateProjectSettingsRequest := * unleash .NewUpdateProjectEnterpriseSettingsSchemaWithDefaults ()
212285 updateProjectSettingsRequest .SetMode (mode )
213286
214- updateSettingsResponse , err := r .client .ProjectsAPI .UpdateProjectEnterpriseSettings (ctx , * state .Id .ValueStringPointer ()).UpdateProjectEnterpriseSettingsSchema (updateProjectSettingsRequest ).Execute ()
287+ featureNaming := expandFeatureNaming (plan .FeatureNaming , & resp .Diagnostics )
288+ if resp .Diagnostics .HasError () {
289+ return
290+ }
291+ if featureNaming != nil {
292+ updateProjectSettingsRequest .SetFeatureNaming (* featureNaming )
293+ }
215294
216- if ! ValidateApiResponse (updateSettingsResponse , 200 , & resp .Diagnostics , err ) {
295+ linkTemplates := expandLinkTemplates (plan .LinkTemplates , & resp .Diagnostics )
296+ if resp .Diagnostics .HasError () {
217297 return
218298 }
299+ if linkTemplates != nil {
300+ updateProjectSettingsRequest .SetLinkTemplates (linkTemplates )
301+ }
219302
220- req .State .Get (ctx , & state )
303+ updateSettingsResponse , err := r .client .ProjectsAPI .UpdateProjectEnterpriseSettings (ctx , * plan .Id .ValueStringPointer ()).UpdateProjectEnterpriseSettingsSchema (updateProjectSettingsRequest ).Execute ()
304+
305+ if ! ValidateApiResponse (updateSettingsResponse , 200 , & resp .Diagnostics , err ) {
306+ return
307+ }
221308
222- api_response , err := r .client .ProjectsAPI .UpdateProject (ctx , state .Id .ValueString ()).UpdateProjectSchema (updateProjectSchema ).Execute ()
309+ api_response , err := r .client .ProjectsAPI .UpdateProject (ctx , plan .Id .ValueString ()).UpdateProjectSchema (updateProjectSchema ).Execute ()
223310
224311 if ! ValidateApiResponse (api_response , 200 , & resp .Diagnostics , err ) {
225312 return
@@ -230,26 +317,26 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest
230317
231318 var project unleash.ProjectSchema
232319 for _ , p := range projects .Projects {
233- if p .Id == state .Id .ValueString () {
320+ if p .Id == plan .Id .ValueString () {
234321 project = p
235322 }
236323 }
237324 if ! ValidateApiResponse (api_response , 200 , & resp .Diagnostics , err ) {
238325 return
239326 }
240327
241- state .Id = types .StringValue (fmt .Sprintf ("%v" , project .Id ))
242- state .Name = types .StringValue (fmt .Sprintf ("%v" , project .Name ))
328+ plan .Id = types .StringValue (fmt .Sprintf ("%v" , project .Id ))
329+ plan .Name = types .StringValue (fmt .Sprintf ("%v" , project .Name ))
243330
244- setModelMode (project .Mode , & state )
331+ setModelMode (project .Mode , & plan )
245332
246333 if project .Description .IsSet () {
247- state .Description = types .StringValue (* project .Description .Get ())
334+ plan .Description = types .StringValue (* project .Description .Get ())
248335 } else {
249- state .Description = types .StringNull ()
336+ plan .Description = types .StringNull ()
250337 }
251338
252- resp .Diagnostics .Append (resp .State .Set (ctx , & state )... )
339+ resp .Diagnostics .Append (resp .State .Set (ctx , & plan )... )
253340 tflog .Debug (ctx , "Finished updating project data source" , map [string ]any {"success" : true })
254341}
255342
@@ -294,3 +381,83 @@ func resolveRequestedMode(plan projectResourceModel) (string, error) {
294381 return "open" , nil
295382 }
296383}
384+
385+ func expandFeatureNaming (model * featureNamingModel , diagnostics * diag.Diagnostics ) * unleash.CreateFeatureNamingPatternSchema {
386+ if model == nil {
387+ return nil
388+ }
389+
390+ if model .Pattern .IsUnknown () {
391+ diagnostics .AddError ("Invalid feature_naming.pattern" , "feature_naming.pattern cannot be unknown" )
392+ return nil
393+ }
394+
395+ if model .Pattern .IsNull () || model .Pattern .ValueString () == "" {
396+ diagnostics .AddError ("Invalid feature_naming.pattern" , "feature_naming.pattern must be provided and cannot be empty" )
397+ return nil
398+ }
399+
400+ featureNaming := unleash.CreateFeatureNamingPatternSchema {}
401+ featureNaming .SetPattern (model .Pattern .ValueString ())
402+
403+ if model .Example .IsUnknown () {
404+ diagnostics .AddError ("Invalid feature_naming.example" , "feature_naming.example cannot be unknown" )
405+ return nil
406+ }
407+
408+ if model .Example .IsNull () {
409+ featureNaming .SetExampleNil ()
410+ } else {
411+ featureNaming .SetExample (model .Example .ValueString ())
412+ }
413+
414+ if model .Description .IsUnknown () {
415+ diagnostics .AddError ("Invalid feature_naming.description" , "feature_naming.description cannot be unknown" )
416+ return nil
417+ }
418+
419+ if model .Description .IsNull () {
420+ featureNaming .SetDescriptionNil ()
421+ } else {
422+ featureNaming .SetDescription (model .Description .ValueString ())
423+ }
424+
425+ return & featureNaming
426+ }
427+
428+ func expandLinkTemplates (models []projectLinkTemplateModel , diagnostics * diag.Diagnostics ) []unleash.ProjectLinkTemplateSchema {
429+ if models == nil {
430+ return nil
431+ }
432+
433+ templates := make ([]unleash.ProjectLinkTemplateSchema , len (models ))
434+
435+ for i , model := range models {
436+ if model .UrlTemplate .IsUnknown () {
437+ diagnostics .AddError ("Invalid link_templates url" , fmt .Sprintf ("link_templates[%d].url_template cannot be unknown" , i ))
438+ return nil
439+ }
440+
441+ if model .UrlTemplate .IsNull () || model .UrlTemplate .ValueString () == "" {
442+ diagnostics .AddError ("Invalid link_templates url" , fmt .Sprintf ("link_templates[%d].url_template must be provided and cannot be empty" , i ))
443+ return nil
444+ }
445+
446+ template := unleash .NewProjectLinkTemplateSchema (model .UrlTemplate .ValueString ())
447+
448+ if model .Title .IsUnknown () {
449+ diagnostics .AddError ("Invalid link_templates title" , fmt .Sprintf ("link_templates[%d].title cannot be unknown" , i ))
450+ return nil
451+ }
452+
453+ if model .Title .IsNull () {
454+ template .SetTitleNil ()
455+ } else {
456+ template .SetTitle (model .Title .ValueString ())
457+ }
458+
459+ templates [i ] = * template
460+ }
461+
462+ return templates
463+ }
0 commit comments