Record and expose approval gates on rollouts

What does this MR do?

Records approval gates on continuous deployment rollouts and exposes their state over GraphQL.

  • ProcessWorkflowEventService now journals a request_approval transition when AutoFlow reports a step_started event for the approval step type, guarded against duplicate retries and against rollouts that already reached a terminal state.
  • ResolveGateService now requires an open approval gate (derived from the transition journal) instead of rollout.paused?, and records the resolution against the rollout's actual state instead of a hardcoded paused/paused pair.
  • Cd::Rollout#open_approval_gate? and Cd::RolloutTransition.open_gate_rollout_ids derive gate state from the journal.
  • Adds CdRollout.awaitingApproval (GraphQL), batch-loaded to avoid N+1 queries.

SQL query plans

Cd::Rollout#open_approval_gate?

Single rollout, used by ResolveGateService and the write-path guard in ProcessWorkflowEventService.

Tested with 300 records seeded on local dev DB.

SELECT "cd_rollout_transitions".*
FROM "cd_rollout_transitions"
WHERE "cd_rollout_transitions"."rollout_id" = $1
  AND "cd_rollout_transitions"."event" IN ('request_approval', 'approve', 'reject')
ORDER BY "cd_rollout_transitions"."created_at" DESC, "cd_rollout_transitions"."id" DESC
LIMIT 1;

Limit  (cost=1.53..2.65 rows=1 width=147) (actual time=0.018..0.018 rows=1 loops=1)
  Buffers: shared hit=12
  ->  Incremental Sort  (cost=1.53..10.51 rows=8 width=147) (actual time=0.018..0.018 rows=1 loops=1)
        Sort Key: created_at DESC, id DESC
        Presorted Key: created_at
        Full-sort Groups: 1  Sort Method: quicksort  Average Memory: 25kB  Peak Memory: 25kB
        Buffers: shared hit=12
        ->  Index Scan Backward using index_cd_rollout_transitions_on_rollout_id_and_created_at on cd_rollout_transitions  (cost=0.28..10.15 rows=8 width=147) (actual time=0.009..0.010 rows=2 loops=1)
              Index Cond: (rollout_id = 325)
              Filter: (event = ANY ('{request_approval,approve,reject}'::text[]))
              Rows Removed by Filter: 4
              Buffers: shared hit=3
Planning:
  Buffers: shared hit=172
Planning Time: 1.411 ms
Execution Time: 0.028 ms

Cd::RolloutTransition.open_gate_rollout_ids

Batched, used by the CdRollout.awaitingApproval GraphQL field's batch loader.

Tested with 300 records seeded on local dev DB.

SELECT DISTINCT ON (rollout_id) rollout_id, event
FROM "cd_rollout_transitions"
WHERE "cd_rollout_transitions"."event" IN ('request_approval', 'approve', 'reject')
  AND "cd_rollout_transitions"."rollout_id" IN ($1, $2, ..., $20)
ORDER BY rollout_id, created_at DESC, id DESC;

Unique  (cost=0.69..59.15 rows=131 width=32) (actual time=0.024..0.085 rows=20 loops=1)
  Buffers: shared hit=9
  ->  Incremental Sort  (cost=0.69..58.73 rows=169 width=32) (actual time=0.024..0.079 rows=176 loops=1)
        Sort Key: rollout_id, created_at DESC, id DESC
        Presorted Key: rollout_id
        Full-sort Groups: 5  Sort Method: quicksort  Average Memory: 27kB  Peak Memory: 27kB
        Buffers: shared hit=9
        ->  Index Scan using index_cd_rollout_transitions_on_rollout_id_and_created_at on cd_rollout_transitions  (cost=0.28..52.45 rows=169 width=32) (actual time=0.006..0.051 rows=176 loops=1)
              Index Cond: (rollout_id = ANY ('{325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344}'::bigint[]))
              Filter: (event = ANY ('{request_approval,approve,reject}'::text[]))
              Rows Removed by Filter: 224
              Buffers: shared hit=9
Planning:
  Buffers: shared hit=9
Planning Time: 0.039 ms
Execution Time: 0.092 ms

GraphQl query

query {
  organization(id: "gid://gitlab/Organizations::Organization/1") {
    cdRollout(id: "gid://gitlab/Cd::Rollout/1") {
      id
      state
      awaitingApproval
      rolloutTransitions {
        nodes {
          event
          principal
          createdAt
        }
      }
    }
  }
}

query

Validation steps

  • create new rollout events in the rails console:
rollout = Cd::Rollout.find(1)

result = Cd::Rollouts::ProcessWorkflowEventService.new(
  rollout,
  params: {
    type: "com.gitlab.cd.step_started",
    data: { position: [0, 0], step_type: Cd::RolloutStep::APPROVAL_STEP_TYPE }
  }
).execute
  • run the query outlined above in the graphql explorer

Related to #607600

Merge request reports

Loading
Loading