Skip to content

Commit edec6ed

Browse files
jamescarrandrewstillv15claude
authored
Add opslevel_campaign resource with full CRUD and check management (#643)
* feat: add opslevel_campaign resource with full CRUD 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 * feat: register opslevel_campaign in provider and enable local SDK Wire up NewCampaignResource in the provider's resource list and enable the local replace directive for opslevel-go development. Made-with: Cursor * docs: add campaign resource documentation and examples Add resource documentation with usage examples for both draft and scheduled campaigns, schema reference, and import instructions. Made-with: Cursor * feat: add opslevel_campaign and opslevel_campaigns data sources 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 * feat: add start_date/target_date pair validation 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 * test: add mock terraform tests for campaign resource 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 * test: add remote integration test for campaign resource 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 * chore: add changie changelog entry for campaign resource Made-with: Cursor * chore: update opslevel-go submodule with campaign CRUD methods Points submodule to jamescarr/add-campaign-crud branch which adds GetCampaign, CreateCampaign, UpdateCampaign, DeleteCampaign, ScheduleCampaign, and UnscheduleCampaign. Made-with: Cursor * fix: pass dates directly in campaign create/update 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 * fix: create then schedule campaign in two API calls 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 * feat: add check_ids field to campaign resource Passes check IDs via checkIdsToCopy on campaign creation to associate existing checks with the campaign. Made-with: Cursor * fix: set CheckIds type in model to prevent DynamicPseudoType panic Made-with: Cursor * feat: use checksCopyToCampaign for associating checks with campaigns 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 * fix: update SDK submodule with deletedId fix for campaign delete Made-with: Cursor * fix: make check_ids create-only (OpsLevel API has no check removal) 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 * feat: reconcile campaign check_ids on update (add + remove) 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 * fix: read campaign checks from API for proper state tracking 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 * extract DiffCheckIds, add unit tests, bump opslevel-go 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 * code review fixes: conventions, docs, tests, and safety - 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 * fix: use nullableID for filter_id to support unsetting 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 * Apply suggestions from code review Co-authored-by: andrewstillv15 <andrew.still@opslevel.com> * bump opslevel go * update opslevel-go * fix: gofumpt formatting and project_brief clear on update Always send projectBrief on update so removing it from config sends "" to the API, which clears the field (null is ignored by the API). Also fixes gofumpt struct tag alignment in CampaignResourceModel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * get pipeline going --------- Co-authored-by: andrewstillv15 <andrew.still@opslevel.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 337ef62 commit edec6ed

24 files changed

Lines changed: 1673 additions & 140 deletions
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
kind: Added
2+
body: Added `opslevel_campaign` resource for managing OpsLevel campaigns as code, including scheduling support. Also added `opslevel_campaign` and `opslevel_campaigns` data sources.

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
version: "2"
12
run:
23
timeout: 2m
34
linters:

docs/data-sources/campaign.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
page_title: "opslevel_campaign Data Source - terraform-provider-opslevel"
3+
subcategory: ""
4+
description: |-
5+
Campaign data source
6+
---
7+
8+
# opslevel_campaign (Data Source)
9+
10+
Campaign data source
11+
12+
## Example Usage
13+
14+
```terraform
15+
data "opslevel_campaign" "example" {
16+
identifier = "Z2lkOi8vb3BzbGV2ZWwvQ2FtcGFpZ24vMTIz"
17+
}
18+
19+
output "campaign_name" {
20+
value = data.opslevel_campaign.example.name
21+
}
22+
```
23+
24+
<!-- schema generated by tfplugindocs -->
25+
## Schema
26+
27+
### Required
28+
29+
- `identifier` (String) The id of the campaign to find.
30+
31+
### Read-Only
32+
33+
- `filter_id` (String) The ID of the filter applied to this campaign.
34+
- `html_url` (String) The URL to the campaign in the OpsLevel UI.
35+
- `id` (String) The ID of the campaign.
36+
- `name` (String) The name of the campaign.
37+
- `owner_id` (String) The ID of the team that owns this campaign.
38+
- `project_brief` (String) The raw project brief of the campaign (Markdown).
39+
- `start_date` (String) The start date of the campaign.
40+
- `status` (String) The current status of the campaign.
41+
- `target_date` (String) The target end date of the campaign.

docs/data-sources/campaigns.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
page_title: "opslevel_campaigns Data Source - terraform-provider-opslevel"
3+
subcategory: ""
4+
description: |-
5+
Campaign data sources
6+
---
7+
8+
# opslevel_campaigns (Data Source)
9+
10+
Campaign data sources — lists all campaigns, optionally filtered by status.
11+
12+
## Example Usage
13+
14+
```terraform
15+
data "opslevel_campaigns" "active" {
16+
status = "in_progress"
17+
}
18+
19+
output "active_campaign_names" {
20+
value = [for c in data.opslevel_campaigns.active.campaigns : c.name]
21+
}
22+
```
23+
24+
<!-- schema generated by tfplugindocs -->
25+
## Schema
26+
27+
### Optional
28+
29+
- `status` (String) Filter campaigns by status (draft, scheduled, in_progress, delayed, ended). Defaults to in_progress.
30+
31+
### Read-Only
32+
33+
- `campaigns` (List of Object) List of Campaign data sources (see [below for nested schema](#nestedatt--campaigns))
34+
35+
<a id="nestedatt--campaigns"></a>
36+
### Nested Schema for `campaigns`
37+
38+
Read-Only:
39+
40+
- `filter_id` (String) The ID of the filter applied to this campaign.
41+
- `html_url` (String) The URL to the campaign in the OpsLevel UI.
42+
- `id` (String) The ID of the campaign.
43+
- `name` (String) The name of the campaign.
44+
- `owner_id` (String) The ID of the team that owns this campaign.
45+
- `project_brief` (String) The raw project brief of the campaign (Markdown).
46+
- `start_date` (String) The start date of the campaign.
47+
- `status` (String) The current status of the campaign.
48+
- `target_date` (String) The target end date of the campaign.

docs/resources/campaign.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
page_title: "opslevel_campaign Resource - terraform-provider-opslevel"
3+
subcategory: ""
4+
description: |-
5+
Campaign Resource
6+
---
7+
8+
# opslevel_campaign (Resource)
9+
10+
Campaign Resource
11+
12+
Manages an OpsLevel campaign. Campaigns allow you to roll out changes across your engineering organization that require orchestrated effort across multiple teams.
13+
14+
## Example Usage
15+
16+
### Draft campaign (no schedule)
17+
18+
```terraform
19+
data "opslevel_team" "platform" {
20+
alias = "platform"
21+
}
22+
23+
data "opslevel_filter" "tier_1" {
24+
filter {
25+
field = "name"
26+
value = "Tier 1 Services"
27+
}
28+
}
29+
30+
resource "opslevel_campaign" "upgrade_rails" {
31+
name = "Upgrade to Rails 7"
32+
owner_id = data.opslevel_team.platform.id
33+
filter_id = data.opslevel_filter.tier_1.id
34+
35+
project_brief = <<-EOT
36+
## Overview
37+
All Rails services must upgrade to Rails 7 by end of Q3.
38+
39+
## What you need to do
40+
1. Update your Gemfile to target Rails 7
41+
2. Run the Rails upgrade checklist
42+
3. Verify all tests pass
43+
EOT
44+
}
45+
```
46+
47+
### Campaign with checks
48+
49+
```terraform
50+
resource "opslevel_campaign" "soc2_compliance" {
51+
name = "SOC2 Compliance Rollout"
52+
owner_id = data.opslevel_team.platform.id
53+
filter_id = data.opslevel_filter.tier_1.id
54+
55+
check_ids = [
56+
opslevel_check_custom_event.secret_rotation.id,
57+
opslevel_check_custom_event.dependency_scanning.id,
58+
]
59+
60+
project_brief = <<-EOT
61+
All Tier 1 services must pass SOC2 checks by end of Q3.
62+
EOT
63+
}
64+
```
65+
66+
### Scheduled campaign
67+
68+
```terraform
69+
resource "opslevel_campaign" "python_upgrade" {
70+
name = "Upgrade to Python 3.12"
71+
owner_id = data.opslevel_team.platform.id
72+
filter_id = data.opslevel_filter.tier_1.id
73+
74+
start_date = "2026-07-01"
75+
target_date = "2026-09-30"
76+
77+
project_brief = <<-EOT
78+
Upgrade all Python services to 3.12 for security and performance.
79+
EOT
80+
}
81+
```
82+
83+
## Check Management
84+
85+
The `check_ids` attribute accepts a list of rubric check IDs. On create, these checks are copied into the campaign. On update, the provider reconciles the list — adding new checks and removing stale ones to match the desired configuration.
86+
87+
Terraform detects drift: if a check is removed from the campaign outside of Terraform (e.g. via the UI), the next `terraform plan` will show it as needing to be re-added.
88+
89+
~> **Note:** The OpsLevel API copies checks into campaigns as separate instances with different IDs but the same name. The provider matches rubric checks to campaign checks by name. If two rubric checks share the same name, the provider may not be able to distinguish them, which can lead to incorrect removal. Ensure rubric check names are unique when using `check_ids`.
90+
91+
## Schedule Management
92+
93+
Setting both `start_date` and `target_date` schedules the campaign. Removing both fields unschedules it back to draft status.
94+
95+
Both fields must be set together — setting only one will result in an error.
96+
97+
<!-- schema generated by tfplugindocs -->
98+
## Schema
99+
100+
### Required
101+
102+
- `name` (String) The name of the campaign.
103+
- `owner_id` (String) The ID of the team that owns this campaign.
104+
105+
### Optional
106+
107+
- `check_ids` (List of String) List of rubric check IDs to associate with this campaign. On create, checks are copied into the campaign. On update, checks are added or removed to match the desired set.
108+
- `filter_id` (String) The ID of the filter applied to this campaign.
109+
- `project_brief` (String) The project brief of the campaign (Markdown).
110+
- `start_date` (String) The start date of the campaign (YYYY-MM-DD). Setting both start_date and target_date schedules the campaign.
111+
- `target_date` (String) The target end date of the campaign (YYYY-MM-DD). Setting both start_date and target_date schedules the campaign.
112+
113+
### Read-Only
114+
115+
- `html_url` (String) The URL to the campaign in the OpsLevel UI.
116+
- `id` (String) The ID of the campaign.
117+
- `status` (String) The current status of the campaign (draft, scheduled, in_progress, delayed, ended).
118+
119+
## Import
120+
121+
Import is supported using the following syntax:
122+
123+
```shell
124+
terraform import opslevel_campaign.example Z2lkOi8vb3BzbGV2ZWwvQ2FtcGFpZ24vMTIz
125+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
terraform import opslevel_campaign.example Z2lkOi8vb3BzbGV2ZWwvQ2FtcGFpZ24vMTIz
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
data "opslevel_team" "platform" {
2+
alias = "platform"
3+
}
4+
5+
data "opslevel_filter" "tier_1" {
6+
filter {
7+
field = "name"
8+
value = "Tier 1 Services"
9+
}
10+
}
11+
12+
resource "opslevel_campaign" "upgrade_rails" {
13+
name = "Upgrade to Rails 7"
14+
owner_id = data.opslevel_team.platform.id
15+
filter_id = data.opslevel_filter.tier_1.id
16+
17+
start_date = "2026-07-01"
18+
target_date = "2026-09-30"
19+
20+
project_brief = <<-EOT
21+
## Overview
22+
All Rails services must upgrade to Rails 7 by end of Q3.
23+
24+
## What you need to do
25+
1. Update your Gemfile to target Rails 7
26+
2. Run the Rails upgrade checklist
27+
3. Verify all tests pass
28+
EOT
29+
}

0 commit comments

Comments
 (0)