diff --git a/github/resource_github_enterprise_organization.go b/github/resource_github_enterprise_organization.go index fc7c36d884..7a4a8e4448 100644 --- a/github/resource_github_enterprise_organization.go +++ b/github/resource_github_enterprise_organization.go @@ -12,6 +12,19 @@ import ( "github.com/shurcooL/githubv4" ) +func isSAMLEnforcementError(err error) bool { + if err == nil { + return false + } + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { + return ghErr.Response != nil && + ghErr.Response.StatusCode == 403 && + strings.Contains(ghErr.Message, "SAML enforcement") + } + return strings.Contains(err.Error(), "Resource protected by organization SAML enforcement") +} + func resourceGithubEnterpriseOrganization() *schema.Resource { return &schema.Resource{ Create: resourceGithubEnterpriseOrganizationCreate, @@ -128,7 +141,17 @@ func resourceGithubEnterpriseOrganizationCreate(data *schema.ResourceData, meta Name: github.Ptr(displayName), }, ) - return err + if err != nil { + if isSAMLEnforcementError(err) { + // The org was created but we can't set description/display_name until the PAT is authorized. + // Clear them from state so next plan will show drift and retry after PAT authorization. + log.Printf("[WARN] Organization %q created but could not set description/display_name due to SAML enforcement. Authorize the PAT and run apply again.", data.Get("name").(string)) + _ = data.Set("description", "") + _ = data.Set("display_name", "") + return nil + } + return err + } } return nil } @@ -301,10 +324,18 @@ func updateDescription(ctx context.Context, data *schema.ResourceData, v3 *githu ctx, orgName, &github.Organization{ - Description: github.Ptr(data.Get("description").(string)), + Description: github.Ptr(newDesc), }, ) - return err + if err != nil { + if isSAMLEnforcementError(err) { + // Reset state to old value so next plan shows drift + log.Printf("[WARN] Could not update description for %q due to SAML enforcement. Authorize the PAT and run apply again.", orgName) + _ = data.Set("description", oldDesc) + return nil + } + return err + } } return nil } @@ -318,10 +349,18 @@ func updateDisplayName(ctx context.Context, data *schema.ResourceData, v4 *githu ctx, orgName, &github.Organization{ - Name: github.Ptr(data.Get("display_name").(string)), + Name: github.Ptr(newDisplayName), }, ) - return err + if err != nil { + if isSAMLEnforcementError(err) { + // Reset state to old value so next plan shows drift + log.Printf("[WARN] Could not update display_name for %q due to SAML enforcement. Authorize the PAT and run apply again.", orgName) + _ = data.Set("display_name", oldDisplayName) + return nil + } + return err + } } return nil } diff --git a/github/resource_github_enterprise_organization_test.go b/github/resource_github_enterprise_organization_test.go index e9616bee77..fc8b380307 100644 --- a/github/resource_github_enterprise_organization_test.go +++ b/github/resource_github_enterprise_organization_test.go @@ -1,15 +1,75 @@ package github import ( + "errors" "fmt" + "net/http" "regexp" "strings" "testing" + "github.com/google/go-github/v81/github" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) +func TestIsSAMLEnforcementError(t *testing.T) { + tests := []struct { + name string + err error + expected bool + }{ + { + name: "nil error", + err: nil, + expected: false, + }, + { + name: "GitHub ErrorResponse with SAML enforcement", + err: &github.ErrorResponse{ + Response: &http.Response{StatusCode: 403}, + Message: "Resource protected by organization SAML enforcement. You must grant your Personal Access token access to this organization.", + }, + expected: true, + }, + { + name: "GitHub ErrorResponse 403 without SAML message", + err: &github.ErrorResponse{ + Response: &http.Response{StatusCode: 403}, + Message: "Forbidden", + }, + expected: false, + }, + { + name: "GitHub ErrorResponse 404", + err: &github.ErrorResponse{ + Response: &http.Response{StatusCode: 404}, + Message: "Not Found", + }, + expected: false, + }, + { + name: "plain error with SAML enforcement message", + err: errors.New("Resource protected by organization SAML enforcement"), + expected: true, + }, + { + name: "plain error without SAML message", + err: errors.New("some other error"), + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := isSAMLEnforcementError(tc.err) + if result != tc.expected { + t.Errorf("isSAMLEnforcementError(%v) = %v, want %v", tc.err, result, tc.expected) + } + }) + } +} + func TestAccGithubEnterpriseOrganization(t *testing.T) { t.Run("creates and updates an enterprise organization without error", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)