Skip to content

NE-2572: use add/del haproxy api calls#763

Open
jcmoraisjr wants to merge 1 commit intoopenshift:masterfrom
jcmoraisjr:NE-2572-add-del-api
Open

NE-2572: use add/del haproxy api calls#763
jcmoraisjr wants to merge 1 commit intoopenshift:masterfrom
jcmoraisjr:NE-2572-add-del-api

Conversation

@jcmoraisjr
Copy link
Copy Markdown
Member

@jcmoraisjr jcmoraisjr commented Apr 10, 2026

DCM currently works by creating blueprint backends and empty backend server slots upfront. These empty slots are updated later with new endpoints, created on scale out operations, without the need to fork-and-reload haproxy. Scale in operations work in a similar way: backend servers are disabled when endpoints are not available anymore.

HAProxy has add/del apis since 2.5, which adds the benefit of not having to create empty slots. This means that we don't need to predict the ideal number of empty servers per backend, so backends can be configured with the currently active endpoints only, and it can scale to any number of new backend servers without the need to reload.

This update is being made in at least two phases:

Phase 1: update config manager (manager.go) and client's backend (backend.go) the minimum to support the new api calls. The priorities are: new approach fully functional and an easier code update to revise.

Phase 2: cleanup dead code, eventually combining types into a single, coherent and simplified struct.

New phases should happen as a revisit on other parts of the code that can be optimized with the new approach, or the code cleanup, like changing route resources and the handling of the haproxy lookup maps.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added direct backend server management capabilities including add, update, enable/disable health checks, and delete operations.
  • Deprecated

    • --max-dynamic-servers flag is now deprecated.
  • Changes

    • Removed dynamic server template generation from HAProxy configuration; static per-endpoint server entries now exclusively managed.
    • Refactored endpoint tracking and server state management for improved control and flexibility.

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Apr 10, 2026
@openshift-ci-robot
Copy link
Copy Markdown
Contributor

openshift-ci-robot commented Apr 10, 2026

@jcmoraisjr: This pull request references NE-2572 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

DCM currently works by creating blueprint backends and empty backend server slots upfront. These empty slots are updated later with new endpoints, created on scale out operations, without the need to fork-and-reload haproxy. Scale in operations work in a similar way: backend servers are disabled when endpoints are not available anymore.

HAProxy has add/del apis since 2.5, which adds the benefit of not having to create empty slots. This means that we don't need to predict the ideal number of empty servers per backend, so backends can be configured with the currently active endpoints only, and it can scale to any number of new backend servers without the need to reload.

This update is being made in at least two phases:

Phase 1: update config manager (manager.go) and client's backend (backend.go) the minimum to support the new api calls. The priorities are: new approach fully functional and an easier code update to revise.

Phase 2: cleanup dead code, eventually combining types into a single, coherent and simplified struct.

New phases should happen as a revisit on other parts of the code that can be optimized with the new approach, or the code cleanup, like changing route resources and the handling of the haproxy lookup maps.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

Walkthrough

Removed HAProxy dynamic-server template generation and MaxDynamicServers config; replaced template-driven dynamic slots with direct HAProxy runtime commands for server add/update/disable/delete and updated interfaces to pass backend and service context.

Changes

Cohort / File(s) Summary
HAProxy template
images/router/haproxy/conf/haproxy-config.template
Deleted dynamic $dynamicConfigManager emission blocks; left static per-endpoint server lines and added comment markers before generated server directives. Attention: template no longer emits server-template/dynamic-cookie-key or GenerateDynamicServerNames output.
CLI / bootstrap
pkg/cmd/infra/router/template.go
Removed MaxDynamicServers option and its flag binding (deprecated flag kept), added propagation of WorkingDir and DefaultDestinationCA into ConfigManagerOptions.
Public types & interface
pkg/router/template/types.go
ConfigManagerOptions drops MaxDynamicServers, adds WorkingDir and DefaultDestinationCA; ServiceAliasConfig gains EndpointTable; ConfigManager interface Register and ReplaceRouteEndpoints signatures now accept backend/service context; template-related methods removed.
HAProxy manager & backend logic
pkg/router/template/configmanager/haproxy/manager.go, pkg/router/template/configmanager/haproxy/backend.go
Removed dynamic-server maps and template-generation helpers; added Backend methods: AddServer, UpdateServer, EnabledHealthCheck, DisableHealthCheck, DeleteServer; implemented HAProxy runtime command execution (add/set/del/enable/disable) and updated Replace/Remove flows to mutate servers directly. Review for API changes, error handling, and HAProxy command expectations.
Router wiring
pkg/router/template/router.go
Updated dynamicConfigManager registration to pass backend and populate EndpointTable; adjusted Replace/Add flows to pass *ServiceUnit and simplified endpoint replacement logic; logging leveled to Info in some places.
Helpers
pkg/router/template/template_helper.go
Exported FirstMatch(pattern string, values ...string) delegating to existing internal helper.
Tests
pkg/router/router_test.go, pkg/router/template/configmanager/haproxy/blueprint_plugin_test.go
Removed tests/assertions tied to dynamic-template slots and verifyhost/server-template behaviors; updated test doubles to new Register and ReplaceRouteEndpoints signatures and removed template-related stubs. Review test coverage for dynamic command paths.

Sequence Diagram(s)

sequenceDiagram
    participant Router
    participant ConfigManager
    participant HAProxyRuntime
    Router->>ConfigManager: Register(id, backend, route)
    Router->>ConfigManager: ReplaceRouteEndpoints(id, svc, oldEPs, newEPs, weight)
    ConfigManager->>ConfigManager: compute added/modified/deleted endpoints
    ConfigManager->>HAProxyRuntime: "add server <backend> <name> addr <ip> [ssl/verify/...]" 
    HAProxyRuntime-->>ConfigManager: OK / error
    ConfigManager->>HAProxyRuntime: "set server <backend>/<name> addr|weight|state ..."
    HAProxyRuntime-->>ConfigManager: OK / error
    ConfigManager->>HAProxyRuntime: "del server <backend>/<name>"
    HAProxyRuntime-->>ConfigManager: OK / error
    ConfigManager-->>Router: result (nil or error)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 9 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Test Structure And Quality ❓ Inconclusive Test files use standard testing.T framework, not Ginkgo; new public Backend methods (AddServer, UpdateServer, EnabledHealthCheck, DisableHealthCheck, DeleteServer) lack unit tests. Clarify if custom check applies to standard Go tests; add comprehensive tests for the new exported Backend methods to ensure proper test coverage.
✅ Passed checks (9 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically references the main technical change: migrating to HAProxy add/del API calls for dynamic server management, removing the previous pre-allocated server slots approach.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Stable And Deterministic Test Names ✅ Passed The PR does not introduce any Ginkgo test names with dynamic information. Modified test files use standard Go test patterns with static string keys.
Microshift Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; only standard Go unit tests were modified to update test infrastructure.
Single Node Openshift (Sno) Test Compatibility ✅ Passed The PR modifies only existing Go unit tests using the standard testing package, not Ginkgo-based e2e tests.
Topology-Aware Scheduling Compatibility ✅ Passed PR modifies only Go source code and HAProxy configuration template with no changes to Kubernetes-level scheduling specifications.
Ote Binary Stdout Contract ✅ Passed PR does not introduce new OTE stdout violations; fmt.Printf in TestMain is pre-existing, and no new process-level stdout writes were added.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed This PR does not add any new Ginkgo e2e tests. The test files modified contain standard Go unit tests, not Ginkgo-based e2e tests, with net deletions rather than additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci bot commented Apr 10, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign candita for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci bot requested review from davidesalerno and gcs278 April 10, 2026 23:51
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/router/template/configmanager/haproxy/backend.go`:
- Around line 374-376: The ca-file path currently uses string(b.name) (HAProxy
backend name) which doesn't match the on-disk PEM name for route-specific
DestinationCACertificate; change the path construction so it uses the
destination CA certificate ID (the key used in cfg.Certificates / the route's
DestinationCACertificate) instead of string(b.name). Concretely, when choosing
the CA file in the conditional that checks defaultDestinationCA and
cfg.Certificates[cfg.Host+"_pod"], build the path with the destination CA ID
variable (use defaultDestinationCA when that's the chosen CA, or the
route-specific DestinationCACertificate ID otherwise) so the
path.Join(workingDir, "router/cacerts", "<destination-ca-id>.pem") matches the
actual written filename. Ensure references to b.name are removed from that
ca-file path logic.

In `@pkg/router/template/configmanager/haproxy/manager.go`:
- Around line 509-519: The code enables health checks for newly added endpoints
but mistakenly re-enables the former single endpoint from entry.activeEndpoints,
which is empty during initial sync; update the block that builds newEPs to
append the former endpoint from oldEndpoints (the pre-scale-up list) instead of
entry.activeEndpoints so the original server gets health checks on a 1→N
scale-up; locate the logic around newEndpoints, addedEndpoints, newEPs and
entry.activeEndpoints in the function in manager.go and replace the use of
entry.activeEndpoints with the appropriate element(s) from oldEndpoints.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 00ae6c70-df1a-4e7b-9f2b-a256506f707a

📥 Commits

Reviewing files that changed from the base of the PR and between 8963907 and 74a07f4.

📒 Files selected for processing (9)
  • images/router/haproxy/conf/haproxy-config.template
  • pkg/cmd/infra/router/template.go
  • pkg/router/router_test.go
  • pkg/router/template/configmanager/haproxy/backend.go
  • pkg/router/template/configmanager/haproxy/blueprint_plugin_test.go
  • pkg/router/template/configmanager/haproxy/manager.go
  • pkg/router/template/router.go
  • pkg/router/template/template_helper.go
  • pkg/router/template/types.go

@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci bot commented Apr 11, 2026

@jcmoraisjr: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-agnostic 74a07f4 link true /test e2e-agnostic
ci/prow/e2e-aws-fips 74a07f4 link true /test e2e-aws-fips

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

DCM currently works by creating blueprint backends and empty backend
server slots upfront. These empty slots are updated later with new
endpoints, created on scale out operations, without the need to
fork-and-reload haproxy. Scale in operations work in a similar way:
backend servers are disabled when endpoints are not available anymore.

HAProxy has add/del apis since 2.5, which adds the benefit of not having
to create empty slots. This means that we don't need to predict the
ideal number of empty servers per backend, so backends can be configured
with the currently active endpoints only, and it can scale to any number
of new backend servers without the need to reload.

This update is being made in at least two phases:

Phase 1: update config manager (manager.go) and client's backend
(backend.go) the minimum to support the new api calls. The priorities
are: new approach fully functional and an easier code update to revise.

Phase 2: cleanup dead code, eventually combining types into a single,
coherent and simplified struct.

New phases should happen as a revisit on other parts of the code that
can be optimized with the new approach, or the code cleanup, like
changing route resources and the handling of the haproxy lookup maps.
@jcmoraisjr jcmoraisjr force-pushed the NE-2572-add-del-api branch from 74a07f4 to c005e0f Compare April 14, 2026 14:50
@openshift-ci-robot
Copy link
Copy Markdown
Contributor

openshift-ci-robot commented Apr 14, 2026

@jcmoraisjr: This pull request references NE-2572 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.22.0" version, but no target version was set.

Details

In response to this:

DCM currently works by creating blueprint backends and empty backend server slots upfront. These empty slots are updated later with new endpoints, created on scale out operations, without the need to fork-and-reload haproxy. Scale in operations work in a similar way: backend servers are disabled when endpoints are not available anymore.

HAProxy has add/del apis since 2.5, which adds the benefit of not having to create empty slots. This means that we don't need to predict the ideal number of empty servers per backend, so backends can be configured with the currently active endpoints only, and it can scale to any number of new backend servers without the need to reload.

This update is being made in at least two phases:

Phase 1: update config manager (manager.go) and client's backend (backend.go) the minimum to support the new api calls. The priorities are: new approach fully functional and an easier code update to revise.

Phase 2: cleanup dead code, eventually combining types into a single, coherent and simplified struct.

New phases should happen as a revisit on other parts of the code that can be optimized with the new approach, or the code cleanup, like changing route resources and the handling of the haproxy lookup maps.

Summary by CodeRabbit

Release Notes

  • New Features

  • Added direct backend server management capabilities including add, update, enable/disable health checks, and delete operations.

  • Deprecated

  • --max-dynamic-servers flag is now deprecated.

  • Changes

  • Removed dynamic server template generation from HAProxy configuration; static per-endpoint server entries now exclusively managed.

  • Refactored endpoint tracking and server state management for improved control and flexibility.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
pkg/router/router_test.go (1)

177-179: Typo in connection info path: "dymmy" should be "dummy".

While this may be pre-existing, since this line is being modified, it's a good opportunity to fix the typo.

✏️ Suggested fix
 		DynamicConfigManager: haproxyconfigmanager.NewHAProxyConfigManager(templateplugin.ConfigManagerOptions{
-			ConnectionInfo: "unix:///var/lib/dymmy",
+			ConnectionInfo: "unix:///var/lib/dummy",
 		}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/router/router_test.go` around lines 177 - 179, The ConnectionInfo string
passed into haproxyconfigmanager.NewHAProxyConfigManager via the
ConfigManagerOptions in the DynamicConfigManager setup contains a typo
("unix:///var/lib/dymmy"); update that value to the correct path
("unix:///var/lib/dummy") so the DynamicConfigManager initialization
(haproxyconfigmanager.NewHAProxyConfigManager and
ConfigManagerOptions.ConnectionInfo) uses the proper connection path.
pkg/router/template/configmanager/haproxy/manager.go (1)

562-570: Inconsistent error handling: fail-fast vs aggregated errors.

RemoveRouteEndpoints returns immediately on the first DeleteServer error (fail-fast), while ReplaceRouteEndpoints aggregates all errors using errors.Join. Consider aligning the error handling patterns for consistency.

♻️ Option to align with ReplaceRouteEndpoints pattern
+	var errs []error
 	for _, ep := range endpoints {
 		log.V(4).Info("deleting server for endpoint", "endpoint", ep.ID)
 		if err := backend.DeleteServer(ep); err != nil {
-			return err
+			errs = append(errs, fmt.Errorf("error deleting server %s: %w", ep.ID, err))
 		}
 	}

-	return nil
+	return errors.Join(errs...)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/router/template/configmanager/haproxy/manager.go` around lines 562 - 570,
RemoveRouteEndpoints currently fails fast on the first backend.DeleteServer
error while ReplaceRouteEndpoints aggregates errors with errors.Join; update
RemoveRouteEndpoints to mirror ReplaceRouteEndpoints by collecting each
DeleteServer error into a slice (or []error), continue deleting remaining
endpoints, log per-endpoint failures with log.V(...) as currently done, and
finally return errors.Join(collectedErrs...) (or nil if none) instead of
returning immediately on the first error.
pkg/router/template/types.go (1)

186-191: Add documentation for new configuration fields.

The WorkingDir and DefaultDestinationCA fields have empty doc comments. These should be documented to clarify their purpose.

📝 Suggested documentation
-	//
+	// WorkingDir is the router's working directory containing configuration
+	// files, certificates, and other router-managed resources.
 	WorkingDir string

-	//
+	// DefaultDestinationCA is the path to the default CA certificate file used
+	// to verify backend server certificates for re-encrypt routes when no
+	// route-specific destination CA is configured.
 	DefaultDestinationCA string
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/router/template/types.go` around lines 186 - 191, Add descriptive doc
comments for the WorkingDir and DefaultDestinationCA struct fields: explain that
WorkingDir is the path the router uses as its runtime working directory for
temporary files/operations, and DefaultDestinationCA is the PEM-encoded CA
certificate (or path/identifier used by your config) applied as the default
trust anchor for outbound TLS destination verification when no per-destination
CA is configured. Update the comments directly above the WorkingDir and
DefaultDestinationCA field declarations in types.go so they succinctly describe
expected value format and purpose for maintainers and users.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/router/template/configmanager/haproxy/manager.go`:
- Around line 495-497: The current early return when errsEndpoints has entries
aborts the subsequent health-check configuration and can leave backends
inconsistent; update the flow in the function that processes endpoints so that
after collecting errsEndpoints you still call the health-check configuration
routine (the existing health check code or method—e.g., configureHealthChecks /
the block immediately following endpoint processing) for successfully processed
backends, collect any health-check errors, and then return a joined error
containing both errsEndpoints and health-check errors (using errors.Join).
Ensure you do not short-circuit before invoking the health-check configuration
and aggregate all errors before returning.

---

Nitpick comments:
In `@pkg/router/router_test.go`:
- Around line 177-179: The ConnectionInfo string passed into
haproxyconfigmanager.NewHAProxyConfigManager via the ConfigManagerOptions in the
DynamicConfigManager setup contains a typo ("unix:///var/lib/dymmy"); update
that value to the correct path ("unix:///var/lib/dummy") so the
DynamicConfigManager initialization
(haproxyconfigmanager.NewHAProxyConfigManager and
ConfigManagerOptions.ConnectionInfo) uses the proper connection path.

In `@pkg/router/template/configmanager/haproxy/manager.go`:
- Around line 562-570: RemoveRouteEndpoints currently fails fast on the first
backend.DeleteServer error while ReplaceRouteEndpoints aggregates errors with
errors.Join; update RemoveRouteEndpoints to mirror ReplaceRouteEndpoints by
collecting each DeleteServer error into a slice (or []error), continue deleting
remaining endpoints, log per-endpoint failures with log.V(...) as currently
done, and finally return errors.Join(collectedErrs...) (or nil if none) instead
of returning immediately on the first error.

In `@pkg/router/template/types.go`:
- Around line 186-191: Add descriptive doc comments for the WorkingDir and
DefaultDestinationCA struct fields: explain that WorkingDir is the path the
router uses as its runtime working directory for temporary files/operations, and
DefaultDestinationCA is the PEM-encoded CA certificate (or path/identifier used
by your config) applied as the default trust anchor for outbound TLS destination
verification when no per-destination CA is configured. Update the comments
directly above the WorkingDir and DefaultDestinationCA field declarations in
types.go so they succinctly describe expected value format and purpose for
maintainers and users.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: da89419c-e254-4010-955c-9113979cb278

📥 Commits

Reviewing files that changed from the base of the PR and between 74a07f4 and c005e0f.

📒 Files selected for processing (9)
  • images/router/haproxy/conf/haproxy-config.template
  • pkg/cmd/infra/router/template.go
  • pkg/router/router_test.go
  • pkg/router/template/configmanager/haproxy/backend.go
  • pkg/router/template/configmanager/haproxy/blueprint_plugin_test.go
  • pkg/router/template/configmanager/haproxy/manager.go
  • pkg/router/template/router.go
  • pkg/router/template/template_helper.go
  • pkg/router/template/types.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/router/template/configmanager/haproxy/blueprint_plugin_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • pkg/router/template/template_helper.go
  • pkg/cmd/infra/router/template.go
  • pkg/router/template/router.go
  • pkg/router/template/configmanager/haproxy/backend.go

Comment on lines +495 to +497
if len(errsEndpoints) > 0 {
return errors.Join(errsEndpoints...)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Early return on endpoint errors skips health check configuration.

If any endpoint operation fails (delete/update/add), the function returns immediately without configuring health checks. This could leave backends in an inconsistent state where some servers were added but health checks weren't enabled.

Consider whether health check configuration should still be attempted for successfully processed endpoints, or document this as intentional fail-fast behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/router/template/configmanager/haproxy/manager.go` around lines 495 - 497,
The current early return when errsEndpoints has entries aborts the subsequent
health-check configuration and can leave backends inconsistent; update the flow
in the function that processes endpoints so that after collecting errsEndpoints
you still call the health-check configuration routine (the existing health check
code or method—e.g., configureHealthChecks / the block immediately following
endpoint processing) for successfully processed backends, collect any
health-check errors, and then return a joined error containing both
errsEndpoints and health-check errors (using errors.Join). Ensure you do not
short-circuit before invoking the health-check configuration and aggregate all
errors before returning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants