Add opslevel_campaign resource with full CRUD and check management#643
Open
jamescarr wants to merge 21 commits intoOpsLevel:mainfrom
Open
Add opslevel_campaign resource with full CRUD and check management#643jamescarr wants to merge 21 commits intoOpsLevel:mainfrom
jamescarr wants to merge 21 commits intoOpsLevel:mainfrom
Conversation
Implements the Terraform resource for managing OpsLevel campaigns: - Create: creates a draft campaign, optionally schedules it - Read: fetches campaign by ID from the API - Update: updates campaign fields and manages schedule lifecycle - Delete: deletes the campaign - ImportState: supports terraform import by campaign ID Managed attributes: name, owner_id, filter_id, project_brief, start_date, target_date. Computed: id, status, html_url. Setting both start_date and target_date triggers scheduling; removing them unschedules the campaign back to draft. Made-with: Cursor
Wire up NewCampaignResource in the provider's resource list and enable the local replace directive for opslevel-go development. Made-with: Cursor
Add resource documentation with usage examples for both draft and scheduled campaigns, schema reference, and import instructions. Made-with: Cursor
Add single-campaign lookup by ID (opslevel_campaign) and list-all with optional status filter (opslevel_campaigns). Follows the same pattern as opslevel_scorecard / opslevel_scorecards. Made-with: Cursor
Add ValidateConfig to opslevel_campaign that ensures both start_date and target_date are set together or both omitted. Prevents confusing API errors from setting only one schedule field. Made-with: Cursor
Add mock provider tests that validate schema attributes, defaults, and plan-time behavior for both full (scheduled) and minimal (draft) campaign configurations. Made-with: Cursor
Add end-to-end test module that creates, updates, and destroys a campaign against a live OpsLevel environment. Also exercises the opslevel_campaigns data source. Made-with: Cursor
Made-with: Cursor
Points submodule to jamescarr/add-campaign-crud branch which adds GetCampaign, CreateCampaign, UpdateCampaign, DeleteCampaign, ScheduleCampaign, and UnscheduleCampaign. Made-with: Cursor
campaignSchedule mutation doesn't exist in the OpsLevel API. Dates are passed in campaignCreate and campaignUpdate inputs instead. Unschedule via campaignUnschedule remains unchanged. Made-with: Cursor
OpsLevel API requires separate mutations: campaignCreate (no dates) followed by campaignScheduleUpdate (sets start/target dates). Same two-step flow for updates. Unschedule still via campaignUnschedule. Made-with: Cursor
Passes check IDs via checkIdsToCopy on campaign creation to associate existing checks with the campaign. Made-with: Cursor
Made-with: Cursor
Replace the broken CheckIdsToCopy field on CampaignCreateInput with the dedicated checksCopyToCampaign GraphQL mutation. Checks are now copied to the campaign after creation (and optional scheduling), ensuring they actually appear in the campaign. Changes: - Call CopyChecksToCampaign after create if check_ids is set - Call CopyChecksToCampaign on update when check_ids changes - Add extractCheckIds helper for type conversion - Update local mock tests to assert on check_ids - Update SDK submodule to v2026.3.7-campaign.2 Made-with: Cursor
Made-with: Cursor
The checksCopyToCampaign mutation is additive — there is no API to remove checks from a campaign. Calling it on update causes duplicates. Remove the update-path copy and document check_ids as create-only. Made-with: Cursor
Author
|
Tested this out in our sandbox: Creationresource "opslevel_campaign" "demo_may_rollout" {
name = "May 2026 Demo Checks Rollout"
owner_id = data.opslevel_team.staff.id
filter_id = data.opslevel_filter.tier1.id
start_date = "2026-05-15"
target_date = "2026-06-30"
check_ids = [
opslevel_check_manual.demo_secret_rotation.id,
opslevel_check_manual.demo_dependency_scanning.id,
opslevel_check_manual.demo_error_budget.id,
opslevel_check_manual.demo_graceful_degradation.id,
opslevel_check_repository_file.demo_adr_docs.id,
]
project_brief = <<-EOT
## Overview
Five new checks are being enabled on **May 15, 2026** as part of the
sandbox demo rollout. All Tier 1 services must pass by **June 30, 2026**.
## Checks Included
| Check | Category | What it verifies |
|-------|----------|-----------------|
| DEMO-001 | Security | Secrets rotated every 90 days |
| DEMO-002 | Security | Dependency vulnerability scanning |
| DEMO-003 | Reliability | Error budget defined and tracked |
| DEMO-004 | Reliability | Graceful degradation under failure |
| DEMO-005 | Quality | Architecture decision records exist |
## What you need to do
1. Review each check's notes for pass criteria
2. Implement any missing requirements
3. Mark manual checks as passing with a comment explaining evidence
## Resources
- [OpsLevel Checks as Code](https://internal.docs)
- Questions? Reach out in #service-opslevel
EOT
}Terraform runs: DeletionModifyingTerraform runs Opslevel |
The Update function now diffs plan vs state check_ids and: - Copies newly added rubric checks to the campaign - Deletes campaign checks matching removed rubric checks (by name) Requires opslevel-go v2026.3.7-campaign.4 for ListCampaignChecks. Made-with: Cursor
Read now queries the campaign's actual checks via ListCampaignChecks and verifies each rubric check ID in state still has a corresponding campaign check. This enables drift detection (UI deletions are noticed) and proper reconciliation on update (adds and removes work correctly). NewCampaignResourceModel no longer handles CheckIds -- each CRUD path sets it explicitly: Create/Update from plan, Read from API verification. Made-with: Cursor
Extracts the check ID diffing logic into a pure, exported function for testability. Adds 9 unit tests covering all edge cases (no change, add-only, remove-only, mixed, empty/nil inputs). Made-with: Cursor
- Add Computed:true to check_ids schema for proper drift handling - Fix provider registration ordering (alphabetical) - Use ComputedStringValue in data source model (convention) - Collapse dead-code date branches in NewCampaignResourceModel - Add check_ids to resource docs with name-matching caveat - Add duplicate-name detection warnings in read and reconcile - Add data source mock test (campaign.tfmock.hcl + tftest) - Expand remote tests to cover schedule/unschedule lifecycle - Add TODO comment on go.mod replace directive for upstream merge - Bump opslevel-go submodule (GetCampaign guard + recursive pagination) Made-with: Cursor
Sends GraphQL null instead of an empty string when filter_id is removed from config. Aligns with how all check resources handle nullable filter IDs. Made-with: Cursor
b7a9084 to
b2fd449
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.









Closes #644
Depends on: OpsLevel/opslevel-go#611
Implements the
opslevel_campaignTerraform resource for managing OpsLevel campaigns as code.Problem
There is no way to manage OpsLevel campaigns as code. Campaign creation, scheduling, check association, and updates must be done manually through the OpsLevel UI, which makes it difficult to version-control campaign rollouts, reproduce them across environments, or compose them alongside the checks and scorecards they reference.
Solution
New resource
opslevel_campaignfollowing the sameCommonResourceClient/ plugin-framework pattern asopslevel_scorecard.Managed attributes:
name,owner_id,filter_id,project_brief,check_ids,start_date,target_date.Computed attributes:
id,status,html_url.CRUD lifecycle:
Create: Creates a draft campaign via
campaignCreate. If bothstart_dateandtarget_dateare set, immediately schedules it viacampaignScheduleUpdate. Ifcheck_idsis set, copies rubric checks into the campaign viachecksCopyToCampaign.Read: Fetches campaign by ID using
account.campaign(id:). Queries the campaign's actual checks from the API viaListCampaignChecks, cross-references them by name against the configured rubric check IDs, and updates state to reflect only checks that still exist in the campaign. This enables drift detection — if checks are removed outside Terraform (e.g. via the UI), the nextplanwill show them as needing to be re-added.Update: Updates campaign fields via
campaignUpdate, then manages schedule lifecycle:ScheduleCampaignUnscheduleCampaign(reverts to draft)ScheduleCampaignwith new valuesReconciles
check_ids— computes a diff between state and plan, adds new checks viaCopyChecksToCampaign, and removes stale checks by looking up the rubric check name, finding the corresponding campaign check instance by name, and deleting it viaDeleteCheck.Delete: Deletes the campaign via
campaignDelete.Import: Supports
terraform importby campaign ID.Data sources:
opslevel_campaign— look up a single campaignopslevel_campaigns— list all campaignsCheck reconciliation detail:
The OpsLevel API copies checks into campaigns as separate instances (different IDs, same name). There is no direct "remove check from campaign" mutation. The provider handles this by:
GetCheckListCampaignChecksDeleteCheckThe diffing logic is extracted into a pure
DiffCheckIdsfunction with full unit test coverage (9 test cases covering add-only, remove-only, mixed, empty, and nil inputs).Example
Tests
tests/local/resource_campaign.tftest.hcl) — attribute passthrough on createtests/remote/campaign.tftest.hcl) — full lifecycle against live APIopslevel/resource_opslevel_campaign_test.go) — 9 test cases forDiffCheckIdsChecklist
approval from product management to change the interface.
entry that explains the customer facing outcome of this change