From ee83bfd7afee3107be1bdf06649162a619afeac5 Mon Sep 17 00:00:00 2001 From: Alexis Couvreur Date: Tue, 5 May 2026 14:57:30 +0200 Subject: [PATCH 1/3] fix: support repository rename in resource_github_actions_repository_oidc_subject_claim_customization_template Related to https://github.com/integrations/terraform-provider-github/issues/3180 --- ...dc_subject_claim_customization_template.go | 98 ++++++++++++++----- ..._claim_customization_template_migration.go | 61 ++++++++++++ 2 files changed, 132 insertions(+), 27 deletions(-) create mode 100644 github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration.go diff --git a/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go b/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go index ce64a8b2d4..c4b3b695cb 100644 --- a/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go +++ b/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go @@ -3,21 +3,27 @@ package github import ( "context" "errors" + "net/http" "github.com/google/go-github/v85/github" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate, - Read: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead, - Update: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate, - Delete: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateStateUpgradeV0, + Version: 0, + }, }, + Schema: map[string]*schema.Schema{ "repository": { Type: schema.TypeString, @@ -25,6 +31,11 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate() *sch Description: "The name of the repository.", ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 100)), }, + "repository_id": { + Type: schema.TypeInt, + Computed: true, + Description: "ID of the repository.", + }, "use_default": { Type: schema.TypeBool, Required: true, @@ -40,10 +51,22 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate() *sch }, }, }, + + CustomizeDiff: customdiff.All( + diffRepository, + ), + + CreateContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate, + ReadContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead, + UpdateContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate, + DeleteContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, } } -func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client repository := d.Get("repository").(string) @@ -53,7 +76,7 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateO includeClaimKeys, hasClaimKeys := d.GetOk("include_claim_keys") if useDefault && hasClaimKeys { - return errors.New("include_claim_keys cannot be set when use_default is true") + return diag.Errorf("include_claim_keys cannot be set when use_default is true") } customOIDCSubjectClaimTemplate := &github.OIDCSubjectClaimCustomTemplate{ @@ -61,11 +84,9 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateO } if includeClaimKeys != nil { - includeClaimKeysVal := includeClaimKeys.([]any) claimsStr := make([]string, len(includeClaimKeysVal)) - for i, v := range includeClaimKeysVal { claimsStr[i] = v.(string) } @@ -73,42 +94,65 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateO customOIDCSubjectClaimTemplate.IncludeClaimKeys = claimsStr } - ctx := context.Background() _, err := client.Actions.SetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repository, customOIDCSubjectClaimTemplate) if err != nil { - return err + return diag.FromErr(err) + } + + repo, _, err := client.Repositories.Get(ctx, owner, repository) + if err != nil { + return diag.FromErr(err) } d.SetId(repository) - return resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(d, meta) + if err := d.Set("repository_id", int(repo.GetID())); err != nil { + return diag.FromErr(err) + } + + return resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(ctx, d, meta) } -func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "id", d.Id()) - repository := d.Id() + client := meta.(*Owner).v3client owner := meta.(*Owner).name - ctx := context.Background() - template, _, err := client.Actions.GetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repository) + repoName := d.Get("repository").(string) + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { - return deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "actions repository oidc subject claim customization template (%s, %s)", owner, repository) + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + tflog.Info(ctx, "Repository not found, removing from state.", map[string]any{"repository": repoName}) + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + template, _, err := client.Actions.GetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repoName) + if err != nil { + return diag.FromErr(deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "actions repository oidc subject claim customization template (%s, %s)", owner, repoName)) } - if err = d.Set("repository", repository); err != nil { - return err + if err = d.Set("repository", repo.GetName()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("repository_id", int(repo.GetID())); err != nil { + return diag.FromErr(err) } if err = d.Set("use_default", template.UseDefault); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("include_claim_keys", template.IncludeClaimKeys); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete(d *schema.ResourceData, meta any) error { +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { // Reset the repository to use the default claims // https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#using-the-default-subject-claims client := meta.(*Owner).v3client @@ -116,14 +160,14 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete( repository := d.Get("repository").(string) owner := meta.(*Owner).name + useDefault := true customOIDCSubjectClaimTemplate := &github.OIDCSubjectClaimCustomTemplate{ - UseDefault: new(true), + UseDefault: &useDefault, } - ctx := context.Background() _, err := client.Actions.SetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repository, customOIDCSubjectClaimTemplate) if err != nil { - return err + return diag.FromErr(err) } return nil diff --git a/github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration.go b/github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration.go new file mode 100644 index 0000000000..546b764967 --- /dev/null +++ b/github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration.go @@ -0,0 +1,61 @@ +package github + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + Description: "The name of the repository.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 100)), + }, + "use_default": { + Type: schema.TypeBool, + Required: true, + Description: "Whether to use the default template or not. If 'true', 'include_claim_keys' must not be set.", + }, + "include_claim_keys": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + Description: "A list of OpenID Connect claims.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateStateUpgradeV0(ctx context.Context, rawState map[string]any, m any) (map[string]any, error) { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + log.Printf("[DEBUG] OIDC Subject Claim Customization Template state before migration: %#v", rawState) + + repoName, ok := rawState["repository"].(string) + if !ok { + return nil, fmt.Errorf("repository not found or is not a string") + } + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return nil, fmt.Errorf("failed to retrieve repository %s: %w", repoName, err) + } + + rawState["repository_id"] = int(repo.GetID()) + + log.Printf("[DEBUG] OIDC Subject Claim Customization Template state after migration: %#v", rawState) + + return rawState, nil +} From 026fe96830d076b531a2fdc7afdf8b877ca066d3 Mon Sep 17 00:00:00 2001 From: Alexis Couvreur Date: Thu, 24 Sep 2026 08:28:14 -0400 Subject: [PATCH 2/3] chore: reset resource file to merge base before merging main --- ...dc_subject_claim_customization_template.go | 98 +++++-------------- 1 file changed, 27 insertions(+), 71 deletions(-) diff --git a/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go b/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go index 8cd895b7f4..a140212fd8 100644 --- a/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go +++ b/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go @@ -3,11 +3,7 @@ package github import ( "context" "errors" - "net/http" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/google/go-github/v88/github" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -15,15 +11,13 @@ import ( func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate() *schema.Resource { return &schema.Resource{ - SchemaVersion: 1, - StateUpgraders: []schema.StateUpgrader{ - { - Type: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateV0().CoreConfigSchema().ImpliedType(), - Upgrade: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateStateUpgradeV0, - Version: 0, - }, + Create: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate, + Read: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead, + Update: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate, + Delete: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, }, - Schema: map[string]*schema.Schema{ "repository": { Type: schema.TypeString, @@ -31,11 +25,6 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate() *sch Description: "The name of the repository.", ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 100)), }, - "repository_id": { - Type: schema.TypeInt, - Computed: true, - Description: "ID of the repository.", - }, "use_default": { Type: schema.TypeBool, Required: true, @@ -51,22 +40,10 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate() *sch }, }, }, - - CustomizeDiff: customdiff.All( - diffRepository, - ), - - CreateContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate, - ReadContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead, - UpdateContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate, - DeleteContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, } } -func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate(d *schema.ResourceData, meta any) error { client := meta.(*Owner).v3client repository := d.Get("repository").(string) @@ -76,7 +53,7 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateO includeClaimKeys, hasClaimKeys := d.GetOk("include_claim_keys") if useDefault && hasClaimKeys { - return diag.Errorf("include_claim_keys cannot be set when use_default is true") + return errors.New("include_claim_keys cannot be set when use_default is true") } customOIDCSubjectClaimTemplate := &github.OIDCSubjectClaimCustomTemplate{ @@ -84,9 +61,11 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateO } if includeClaimKeys != nil { + includeClaimKeysVal := includeClaimKeys.([]any) claimsStr := make([]string, len(includeClaimKeysVal)) + for i, v := range includeClaimKeysVal { claimsStr[i] = v.(string) } @@ -94,65 +73,42 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateO customOIDCSubjectClaimTemplate.IncludeClaimKeys = claimsStr } + ctx := context.Background() _, err := client.Actions.SetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repository, customOIDCSubjectClaimTemplate) if err != nil { - return diag.FromErr(err) - } - - repo, _, err := client.Repositories.Get(ctx, owner, repository) - if err != nil { - return diag.FromErr(err) + return err } d.SetId(repository) - if err := d.Set("repository_id", int(repo.GetID())); err != nil { - return diag.FromErr(err) - } - - return resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(ctx, d, meta) + return resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(d, meta) } -func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - ctx = tflog.SetField(ctx, "id", d.Id()) - +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(d *schema.ResourceData, meta any) error { client := meta.(*Owner).v3client - owner := meta.(*Owner).name - - repoName := d.Get("repository").(string) - repo, _, err := client.Repositories.Get(ctx, owner, repoName) - if err != nil { - var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { - tflog.Info(ctx, "Repository not found, removing from state.", map[string]any{"repository": repoName}) - d.SetId("") - return nil - } - return diag.FromErr(err) - } + repository := d.Id() + owner := meta.(*Owner).name - template, _, err := client.Actions.GetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repoName) + ctx := context.Background() + template, _, err := client.Actions.GetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repository) if err != nil { - return diag.FromErr(deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "actions repository oidc subject claim customization template (%s, %s)", owner, repoName)) + return deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "actions repository oidc subject claim customization template (%s, %s)", owner, repository) } - if err = d.Set("repository", repo.GetName()); err != nil { - return diag.FromErr(err) - } - if err = d.Set("repository_id", int(repo.GetID())); err != nil { - return diag.FromErr(err) + if err = d.Set("repository", repository); err != nil { + return err } if err = d.Set("use_default", template.UseDefault); err != nil { - return diag.FromErr(err) + return err } if err = d.Set("include_claim_keys", template.IncludeClaimKeys); err != nil { - return diag.FromErr(err) + return err } return nil } -func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete(d *schema.ResourceData, meta any) error { // Reset the repository to use the default claims // https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#using-the-default-subject-claims client := meta.(*Owner).v3client @@ -160,14 +116,14 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete( repository := d.Get("repository").(string) owner := meta.(*Owner).name - useDefault := true customOIDCSubjectClaimTemplate := &github.OIDCSubjectClaimCustomTemplate{ - UseDefault: &useDefault, + UseDefault: new(true), } + ctx := context.Background() _, err := client.Actions.SetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repository, customOIDCSubjectClaimTemplate) if err != nil { - return diag.FromErr(err) + return err } return nil From e92062edf48cd03f42ef58601c02007d3542ade3 Mon Sep 17 00:00:00 2001 From: Alexis Couvreur Date: Thu, 24 Sep 2026 08:31:44 -0400 Subject: [PATCH 3/3] fix: support repository rename in github_actions_repository_oidc_subject_claim_customization_template Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X9qLeC9Lrh4L5NG63qw3rg --- ...dc_subject_claim_customization_template.md | 5 + ...dc_subject_claim_customization_template.go | 194 +++++++++++++----- ..._claim_customization_template_migration.go | 29 +-- ...m_customization_template_migration_test.go | 90 ++++++++ ...bject_claim_customization_template_test.go | 80 ++++++++ ...bject_claim_customization_template.md.tmpl | 5 + 6 files changed, 330 insertions(+), 73 deletions(-) create mode 100644 github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration_test.go diff --git a/docs/resources/actions_repository_oidc_subject_claim_customization_template.md b/docs/resources/actions_repository_oidc_subject_claim_customization_template.md index 4ea052c75b..b94841975f 100644 --- a/docs/resources/actions_repository_oidc_subject_claim_customization_template.md +++ b/docs/resources/actions_repository_oidc_subject_claim_customization_template.md @@ -36,9 +36,14 @@ resource "github_actions_repository_oidc_subject_claim_customization_template" " The following arguments are supported: +- `repository` - (Required) The name of the repository. - `use_default` - (Required) Whether to use the default template or not. If `true`, `include_claim_keys` must not be set. - `include_claim_keys` - (Optional) A list of OpenID Connect claims. +## Attributes Reference + +- `repository_id` - The ID of the repository. + ## Import This resource can be imported using the repository's name. diff --git a/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go b/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go index 9b8d41d763..1a0bc2b71e 100644 --- a/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go +++ b/github/resource_github_actions_repository_oidc_subject_claim_customization_template.go @@ -3,21 +3,38 @@ package github import ( "context" "errors" + "net/http" "github.com/google/go-github/v92/github" + "github.com/hashicorp/terraform-plugin-log/tflog" + "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" ) func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate, - Read: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead, - Update: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate, - Delete: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete, + CreateContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreate, + ReadContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead, + UpdateContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateUpdate, + DeleteContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateImport, }, + + CustomizeDiff: diffRepository, + + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateInstanceStateUpgradeV0, + Version: 0, + }, + }, + + Description: "Creates and manages an OpenID Connect subject claim customization template for a repository.", + Schema: map[string]*schema.Schema{ "repository": { Type: schema.TypeString, @@ -25,6 +42,11 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate() *sch Description: "The name of the repository.", ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 100)), }, + "repository_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The ID of the repository.", + }, "use_default": { Type: schema.TypeBool, Required: true, @@ -43,88 +65,152 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate() *sch } } -func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreateOrUpdate(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + client := meta.v3client + owner := meta.name - repository := d.Get("repository").(string) - owner := meta.(*Owner).name + repoName := d.Get("repository").(string) - useDefault := d.Get("use_default").(bool) - includeClaimKeys, hasClaimKeys := d.GetOk("include_claim_keys") + template, err := expandRepositoryOIDCSubjectClaimCustomTemplate(d) + if err != nil { + return diag.FromErr(err) + } - if useDefault && hasClaimKeys { - return errors.New("include_claim_keys cannot be set when use_default is true") + tflog.Debug(ctx, "Creating repository OIDC subject claim customization template", map[string]any{"repository": repoName}) + + if _, err := client.Actions.SetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repoName, template); err != nil { + return diag.FromErr(err) + } + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return diag.FromErr(err) } - customOIDCSubjectClaimTemplate := github.OIDCSubjectClaimCustomTemplate{ - UseDefault: &useDefault, + d.SetId(repoName) + + if err := d.Set("repository_id", int(repo.GetID())); err != nil { + return diag.FromErr(err) } - if includeClaimKeys != nil { + return nil +} - includeClaimKeysVal := includeClaimKeys.([]any) +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + client := meta.v3client + owner := meta.name - claimsStr := make([]string, len(includeClaimKeysVal)) + repoName := d.Get("repository").(string) - for i, v := range includeClaimKeysVal { - claimsStr[i] = v.(string) + template, _, err := client.Actions.GetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repoName) + if err != nil { + if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == http.StatusNotFound { + tflog.Info(ctx, "Removing repository OIDC subject claim customization template from state because it no longer exists in GitHub", map[string]any{"repository": repoName}) + d.SetId("") + return nil } - - customOIDCSubjectClaimTemplate.IncludeClaimKeys = claimsStr + return diag.FromErr(err) } - ctx := context.Background() - _, err := client.Actions.SetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repository, customOIDCSubjectClaimTemplate) - if err != nil { - return err + if err := d.Set("use_default", template.UseDefault); err != nil { + return diag.FromErr(err) + } + if err := d.Set("include_claim_keys", template.IncludeClaimKeys); err != nil { + return diag.FromErr(err) } - d.SetId(repository) - return resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(d, meta) + return nil } -func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateRead(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + client := meta.v3client + owner := meta.name - repository := d.Id() - owner := meta.(*Owner).name + repoName := d.Get("repository").(string) - ctx := context.Background() - template, _, err := client.Actions.GetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repository) + template, err := expandRepositoryOIDCSubjectClaimCustomTemplate(d) if err != nil { - return deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "actions repository oidc subject claim customization template (%s, %s)", owner, repository) + return diag.FromErr(err) } - if err = d.Set("repository", repository); err != nil { - return err - } - if err = d.Set("use_default", template.UseDefault); err != nil { - return err - } - if err = d.Set("include_claim_keys", template.IncludeClaimKeys); err != nil { - return err + tflog.Debug(ctx, "Updating repository OIDC subject claim customization template", map[string]any{"repository": repoName}) + + if _, err := client.Actions.SetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repoName, template); err != nil { + return diag.FromErr(err) } + d.SetId(repoName) + return nil } -func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete(d *schema.ResourceData, meta any) error { - // Reset the repository to use the default claims - // https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#using-the-default-subject-claims - client := meta.(*Owner).v3client +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + client := meta.v3client + owner := meta.name + + repoName := d.Get("repository").(string) - repository := d.Get("repository").(string) - owner := meta.(*Owner).name + tflog.Debug(ctx, "Resetting repository OIDC subject claim customization template to default", map[string]any{"repository": repoName}) - customOIDCSubjectClaimTemplate := github.OIDCSubjectClaimCustomTemplate{ + // Delete resets the repository to the default claims. + // https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#using-the-default-subject-claims + template := github.OIDCSubjectClaimCustomTemplate{ UseDefault: new(true), } - ctx := context.Background() - _, err := client.Actions.SetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repository, customOIDCSubjectClaimTemplate) - if err != nil { - return err + if _, err := client.Actions.SetRepoOIDCSubjectClaimCustomTemplate(ctx, owner, repoName, template); err != nil { + if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == http.StatusNotFound { + tflog.Info(ctx, "Repository no longer exists, nothing to reset", map[string]any{"repository": repoName}) + return nil + } + return diag.FromErr(err) } return nil } + +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { + meta, _ := m.(*Owner) + client := meta.v3client + owner := meta.name + + repoName := d.Id() + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return nil, err + } + + if err := d.Set("repository", repo.GetName()); err != nil { + return nil, err + } + if err := d.Set("repository_id", int(repo.GetID())); err != nil { + return nil, err + } + d.SetId(repo.GetName()) + + return []*schema.ResourceData{d}, nil +} + +func expandRepositoryOIDCSubjectClaimCustomTemplate(d *schema.ResourceData) (github.OIDCSubjectClaimCustomTemplate, error) { + useDefault := d.Get("use_default").(bool) + includeClaimKeys, hasClaimKeys := d.GetOk("include_claim_keys") + + if useDefault && hasClaimKeys { + return github.OIDCSubjectClaimCustomTemplate{}, errors.New("include_claim_keys cannot be set when use_default is true") + } + + template := github.OIDCSubjectClaimCustomTemplate{ + UseDefault: new(useDefault), + } + + if hasClaimKeys { + template.IncludeClaimKeys = expandStringList(includeClaimKeys.([]any)) + } + + return template, nil +} diff --git a/github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration.go b/github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration.go index 546b764967..511fe8776e 100644 --- a/github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration.go +++ b/github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration.go @@ -2,14 +2,13 @@ package github import ( "context" - "fmt" - "log" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateV0() *schema.Resource { +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateResourceV0() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ "repository": { @@ -36,26 +35,18 @@ func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateV0() *s } } -func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateStateUpgradeV0(ctx context.Context, rawState map[string]any, m any) (map[string]any, error) { - meta := m.(*Owner) +func resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, m any) (map[string]any, error) { + tflog.Debug(ctx, "Migrating repository OIDC subject claim customization template from v0 to v1.", rawState) + + meta, _ := m.(*Owner) client := meta.v3client owner := meta.name - log.Printf("[DEBUG] OIDC Subject Claim Customization Template state before migration: %#v", rawState) - - repoName, ok := rawState["repository"].(string) - if !ok { - return nil, fmt.Errorf("repository not found or is not a string") - } - - repo, _, err := client.Repositories.Get(ctx, owner, repoName) + migratedState, err := migrateRepositoryWithID(ctx, client, owner, rawState) if err != nil { - return nil, fmt.Errorf("failed to retrieve repository %s: %w", repoName, err) + return nil, err } - rawState["repository_id"] = int(repo.GetID()) - - log.Printf("[DEBUG] OIDC Subject Claim Customization Template state after migration: %#v", rawState) - - return rawState, nil + tflog.Debug(ctx, "Migrated repository OIDC subject claim customization template to v1.", migratedState) + return migratedState, nil } diff --git a/github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration_test.go b/github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration_test.go new file mode 100644 index 0000000000..e77641606a --- /dev/null +++ b/github/resource_github_actions_repository_oidc_subject_claim_customization_template_migration_test.go @@ -0,0 +1,90 @@ +package github + +import ( + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-github/v92/github" +) + +func Test_resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateInstanceStateUpgradeV0(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + testName string + statusCode int + body *github.Repository + rawState map[string]any + want map[string]any + wantErr *string + }{ + { + testName: "adds_repository_id", + statusCode: 200, + body: &github.Repository{ + ID: new(int64(1234567890)), + Name: new("test-repo"), + }, + rawState: map[string]any{ + "id": "test-repo", + "repository": "test-repo", + "use_default": false, + "include_claim_keys": []any{"repo", "context"}, + }, + want: map[string]any{ + "id": "test-repo", + "repository": "test-repo", + "repository_id": 1234567890, + "use_default": false, + "include_claim_keys": []any{"repo", "context"}, + }, + }, + { + testName: "fails_if_repo_not_found", + statusCode: 404, + body: nil, + rawState: map[string]any{ + "id": "test-repo", + "repository": "test-repo", + }, + wantErr: new("failed to retrieve repository"), + }, + { + testName: "fails_if_repository_missing", + statusCode: 404, + body: nil, + rawState: map[string]any{ + "id": "test-repo", + }, + wantErr: new("repository name not found in state"), + }, + } { + t.Run(tt.testName, func(t *testing.T) { + t.Parallel() + + ts := githubApiMock([]*mockResponse{mustGetTestMockResponse(t, "/repos/test-org/test-repo", tt.statusCode, tt.body)}) + defer ts.Close() + meta := &Owner{name: "test-org", v3client: mustCreateTestGitHubClient(t, ts.URL)} + + got, err := resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplateInstanceStateUpgradeV0(t.Context(), tt.rawState, meta) + if err != nil { + if tt.wantErr == nil { + t.Fatalf("unexpected error: %s", err) + } + if !regexp.MustCompile(regexp.QuoteMeta(*tt.wantErr)).MatchString(err.Error()) { + t.Fatalf("unexpected error: %s", err) + } + return + } + + if tt.wantErr != nil { + t.Fatalf("expected error: %s", *tt.wantErr) + } + + if diff := cmp.Diff(got, tt.want); diff != "" { + t.Fatalf("got %+v, want %+v, diff %s", got, tt.want, diff) + } + }) + } +} diff --git a/github/resource_github_actions_repository_oidc_subject_claim_customization_template_test.go b/github/resource_github_actions_repository_oidc_subject_claim_customization_template_test.go index 987a4ae7be..c0e3327c6e 100644 --- a/github/resource_github_actions_repository_oidc_subject_claim_customization_template_test.go +++ b/github/resource_github_actions_repository_oidc_subject_claim_customization_template_test.go @@ -6,6 +6,10 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) func TestAccGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate(t *testing.T) { @@ -236,4 +240,80 @@ func TestAccGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate(t *test }, }) }) + + t.Run("updates_renamed_repo", func(t *testing.T) { + t.Parallel() + skipUnauthenticated(t) + + repo := mustCreateTestRepository(t) + newRepoName := fmt.Sprintf("%s-updated", repo.GetName()) + + config := ` +resource "github_actions_repository_oidc_subject_claim_customization_template" "test" { + repository = "%s" + use_default = false + include_claim_keys = ["repo", "context"] +} +` + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repo.GetName()), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_actions_repository_oidc_subject_claim_customization_template.test", tfjsonpath.New("repository_id"), knownvalue.Int64Exact(repo.GetID())), + }, + }, + { + PreConfig: func() { + mustRenameTestRepository(t, repo, newRepoName) + }, + Config: fmt.Sprintf(config, newRepoName), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_actions_repository_oidc_subject_claim_customization_template.test", plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_actions_repository_oidc_subject_claim_customization_template.test", tfjsonpath.New("repository"), knownvalue.StringExact(newRepoName)), + statecheck.ExpectKnownValue("github_actions_repository_oidc_subject_claim_customization_template.test", tfjsonpath.New("repository_id"), knownvalue.Int64Exact(repo.GetID())), + }, + }, + }, + }) + }) + + t.Run("recreates_changed_repo", func(t *testing.T) { + t.Parallel() + skipUnauthenticated(t) + + repo := mustCreateTestRepository(t) + repo2 := mustCreateTestRepository(t) + + config := ` +resource "github_actions_repository_oidc_subject_claim_customization_template" "test" { + repository = "%s" + use_default = false + include_claim_keys = ["repo", "context"] +} +` + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repo.GetName()), + }, + { + Config: fmt.Sprintf(config, repo2.GetName()), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_actions_repository_oidc_subject_claim_customization_template.test", plancheck.ResourceActionReplace), + }, + }, + }, + }, + }) + }) } diff --git a/templates/resources/actions_repository_oidc_subject_claim_customization_template.md.tmpl b/templates/resources/actions_repository_oidc_subject_claim_customization_template.md.tmpl index f66796c378..416e51e519 100644 --- a/templates/resources/actions_repository_oidc_subject_claim_customization_template.md.tmpl +++ b/templates/resources/actions_repository_oidc_subject_claim_customization_template.md.tmpl @@ -26,9 +26,14 @@ The following table lists the behaviour of `use_default`: The following arguments are supported: +- `repository` - (Required) The name of the repository. - `use_default` - (Required) Whether to use the default template or not. If `true`, `include_claim_keys` must not be set. - `include_claim_keys` - (Optional) A list of OpenID Connect claims. +## Attributes Reference + +- `repository_id` - The ID of the repository. + ## Import This resource can be imported using the repository's name.