Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions examples/resources/okta_campaign/basic.tf
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,19 @@ resource "okta_campaign" "test" {

schedule_settings {
type = "ONE_OFF"
start_date = "2025-10-04T13:43:40.000Z"
start_date = "2026-10-04T13:43:40.000Z"
duration_in_days = 21
time_zone = "America/Vancouver"
}

resource_settings {
type = "APPLICATION"
include_entitlements = true
include_entitlements = false
individually_assigned_apps_only = false
individually_assigned_groups_only = false
only_include_out_of_policy_entitlements = false
target_resources {
resource_id = "0oao01ardu8r8qUP91d7"
resource_type = "APPLICATION"
include_all_entitlements_and_bundles = true
}
target_resources {
resource_id = "0oanlpd3xkLkePi3W1d7"
resource_id = "0oaws4am895IZbn6Q1d7"
resource_type = "APPLICATION"
include_all_entitlements_and_bundles = false
}
Expand Down
59 changes: 59 additions & 0 deletions examples/resources/okta_campaign/updated.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
resource "okta_user" "test" {
first_name = "TestAcc"
last_name = "Smith-1"
login = "testAcc-replace_with_uuid@example.com"
email = "testAcc-replace_with_uuid@example.com"
}

resource "okta_campaign" "test" {
name = "Quarterly access review of sales team"
description = "Multi app campaign"
campaign_type = "RESOURCE"

schedule_settings {
type = "ONE_OFF"
start_date = "2026-10-04T13:43:40.000Z"
duration_in_days = 21
time_zone = "America/Vancouver"
}

resource_settings {
type = "APPLICATION"
include_entitlements = false
individually_assigned_apps_only = false
individually_assigned_groups_only = false
only_include_out_of_policy_entitlements = false
target_resources {
resource_id = "0oaws4am895IZbn6Q1d7"
resource_type = "APPLICATION"
include_all_entitlements_and_bundles = false
}
}

principal_scope_settings {
type = "USERS"
include_only_active_users = false
}

reviewer_settings {
type = "USER"
reviewer_id = okta_user.test.id
self_review_disabled = true
justification_required = true
bulk_decision_disabled = true
}

notification_settings {
notify_reviewer_when_review_assigned = false
notify_reviewer_at_campaign_end = false
notify_reviewer_when_overdue = false
notify_reviewer_during_midpoint_of_review = false
notify_review_period_end = false
}

remediation_settings {
access_approved = "NO_ACTION"
access_revoked = "NO_ACTION"
no_response = "NO_ACTION"
}
}
44 changes: 43 additions & 1 deletion okta/services/governance/resource_campaign.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/okta/okta-governance-sdk-golang/governance"
Expand Down Expand Up @@ -188,10 +193,16 @@ func (r *campaignResource) Schema(ctx context.Context, req resource.SchemaReques
"name": schema.StringAttribute{
Required: true,
Description: "Name of the campaign. Maintain some uniqueness when naming the campaign as it helps to identify and filter for campaigns when needed.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"campaign_tier": schema.StringAttribute{
Optional: true,
Description: "Indicates the minimum required SKU to manage the campaign. Values can be `BASIC` and `PREMIUM`.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"campaign_type": schema.StringAttribute{
Optional: true,
Expand All @@ -200,18 +211,30 @@ func (r *campaignResource) Schema(ctx context.Context, req resource.SchemaReques
Validators: []validator.String{
stringvalidator.OneOf("RESOURCE", "USER"),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"description": schema.StringAttribute{
Optional: true,
Description: "Description about the campaign.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"skip_remediation": schema.BoolAttribute{
Optional: true,
Description: "If true, skip remediation when ending the campaign (only applicable if remediationSetting.noResponse=DENY).",
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(),
},
},
},
Blocks: map[string]schema.Block{
"remediation_settings": schema.SingleNestedBlock{
PlanModifiers: []planmodifier.Object{
objectplanmodifier.RequiresReplace(),
},
Attributes: map[string]schema.Attribute{
"access_approved": schema.StringAttribute{
Required: true,
Expand Down Expand Up @@ -255,6 +278,9 @@ func (r *campaignResource) Schema(ctx context.Context, req resource.SchemaReques
Description: "Specify the action to be taken after a reviewer makes a decision to APPROVE or REVOKE the access, or if the campaign was CLOSED and there was no response from the reviewer.",
},
"resource_settings": schema.SingleNestedBlock{
PlanModifiers: []planmodifier.Object{
objectplanmodifier.RequiresReplace(),
},
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Required: true,
Expand Down Expand Up @@ -380,6 +406,9 @@ func (r *campaignResource) Schema(ctx context.Context, req resource.SchemaReques
Description: "Resource specific properties.",
},
"reviewer_settings": schema.SingleNestedBlock{
PlanModifiers: []planmodifier.Object{
objectplanmodifier.RequiresReplace(),
},
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Required: true,
Expand Down Expand Up @@ -496,6 +525,9 @@ func (r *campaignResource) Schema(ctx context.Context, req resource.SchemaReques
Description: "Identifies the kind of reviewer for Access Certification.",
},
"schedule_settings": schema.SingleNestedBlock{
PlanModifiers: []planmodifier.Object{
objectplanmodifier.RequiresReplace(),
},
Attributes: map[string]schema.Attribute{
"start_date": schema.StringAttribute{
Required: true,
Expand Down Expand Up @@ -546,6 +578,9 @@ func (r *campaignResource) Schema(ctx context.Context, req resource.SchemaReques
Description: "Scheduler specific settings.",
},
"notification_settings": schema.SingleNestedBlock{
PlanModifiers: []planmodifier.Object{
objectplanmodifier.RequiresReplace(),
},
Attributes: map[string]schema.Attribute{
"notify_reviewer_at_campaign_end": schema.BoolAttribute{
Required: true,
Expand All @@ -572,10 +607,16 @@ func (r *campaignResource) Schema(ctx context.Context, req resource.SchemaReques
Computed: true,
ElementType: types.Int64Type,
Description: "Specifies times (in seconds) to send reminders to reviewers before the campaign closes. Max 3 values. Example: [86400, 172800, 604800]",
PlanModifiers: []planmodifier.List{
listplanmodifier.RequiresReplace(),
},
},
},
},
"principal_scope_settings": schema.SingleNestedBlock{
PlanModifiers: []planmodifier.Object{
objectplanmodifier.RequiresReplace(),
},
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Required: true,
Expand Down Expand Up @@ -705,7 +746,8 @@ func (r *campaignResource) Update(ctx context.Context, req resource.UpdateReques

resp.Diagnostics.AddError(
"Update Not Supported",
"No other fields other than launch_campaign and end_campaign are updatable for this resource. Terraform will retain the existing state.",
"The okta_campaign resource does not support in-place updates. All attributes require replacement. "+
"If you see this error, please destroy the resource and re-create it again.",
)
}

Expand Down
52 changes: 52 additions & 0 deletions okta/services/governance/resource_campaign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,55 @@ func TestAccCampaignResource_basic(t *testing.T) {
},
})
}

// TestAccCampaignResource_requiresReplace verifies that mutating any attribute
// that cannot be updated in-place (e.g. name) causes Terraform to destroy and
// re-create the campaign instead of attempting an in-place update that the API
// would reject. The test captures the resource ID after the initial create and
// asserts it differs after the attribute change, confirming a replace occurred.
func TestAccCampaignResource_requiresReplace(t *testing.T) {
mgr := newFixtureManager("resources", resources.OktaGovernanceCampaign, t.Name())
config := mgr.GetFixtures("basic.tf", t)
updatedConfig := mgr.GetFixtures("updated.tf", t)
resourceName := fmt.Sprintf("%s.test", resources.OktaGovernanceCampaign)

var idBeforeReplace string

acctest.OktaResourceTest(t, resource.TestCase{
PreCheck: acctest.AccPreCheck(t),
ErrorCheck: testAccErrorChecks(t),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactoriesForTestAcc(t),
CheckDestroy: nil,
Steps: []resource.TestStep{
{
// Step 1: create the campaign and capture its ID.
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", "Monthly access review of sales team"),
resource.TestCheckResourceAttr(resourceName, "campaign_type", "RESOURCE"),
resource.TestCheckResourceAttrWith(resourceName, "id", func(id string) error {
idBeforeReplace = id
return nil
}),
),
},
{
// Step 2: change `name` — a RequiresReplace attribute.
// Terraform must destroy the old campaign and create a new one
// rather than attempting an unsupported in-place update.
Config: updatedConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", "Quarterly access review of sales team"),
resource.TestCheckResourceAttr(resourceName, "campaign_type", "RESOURCE"),
// The resource ID must differ, proving a replace (destroy+create) occurred.
resource.TestCheckResourceAttrWith(resourceName, "id", func(id string) error {
if id == idBeforeReplace {
return fmt.Errorf("expected campaign ID to change after replace, but it remained %q", id)
}
return nil
}),
),
},
},
})
}
Loading