An overview of the set of standardized guidelines and practices to ensure that the APIs across different projects and teams adhere to a common structure and design. This consistency simplifies the process of understanding and maintaining APIs. These guidelines help identify potential pitfalls and design choices that could lead to issues to help developers avoid common mistakes and build more robust APIs. When in doubt, It is encouraged to seek resolution in slack channel #forum-acs-api-design.
While certain data may exist in the database for internal purposes or other functionalities, it is important to exercise caution when deciding what data to make accessible through the API. The rule of thumb to follow is that the APIs must not expose data structures defined for internal purposes. Instead, define a data structure for the sole purpose of API use, generally in the same package where the API is defined. This applies to all APIs including the inter-component APIs.
- All services defined in the package /proto/api/v2 must not import from the following packages:
- All new services and methods defined in the package /proto/api/v1 must not import from the following packages:
- All new messages defined in the package /proto/internalapi must not import from the following packages:
- All services of the type
APIServiceWithCustomRoutesmust not import from the following packages: - Structs used by APIs, such as those defined in /proto/api/, /proto/internalapi, must not be written to database.
Exposing internal data structures, especially the ones representing and affecting database schema directly at the API can pose security risks. By carefully selecting and limiting the data exposed via the API, we can prevent unauthorized access and potential data breaches which can have compliance ramifications.
Furthermore, exposing unnecessary data in the API can lead to overfetching, where the client receives more data than required. This inefficiency increases network traffic, consumes extra bandwidth, and impacts API performance. By limiting API data to what is actually needed by clients, we optimize resource usage and improve response times.
Consider the following database/internal structures as an example data structure used in Central.
Deployment {
string id
string name
repeated KeyValue labels
repeated KeyValue annotations
repeated Container containers
}
Container {
string name
string image_name
repeated Volume volumes
}
Exposing the above data structure in the API will always lead to reading all the Container bytes from the database,
even if the user does not need them. Instead, design the API such that users can request the information
they need, as below:
// GetDeploymentRequest requests deployment information. Deployment metadata is requested by default.
GetDeploymentRequest {
Options {
bool get_container_spec `json: "getContainerSpec"`
}
}
// Default response
GetDeploymentResponse {
Metadata {
string name `json: "name"`
string id `json: "id"`
}
}
// If `"getContainerSpec": true`
GetDeploymentResponse {
Metadata {
string name `json: "name"`
string id `json: "name"`
...
}
ContainerSpec {
string name `json: "name"`
string image_name `json: "imageName"`
...
}
}
As APIs evolve over time, adding or removing data fields becomes inevitable. By limiting the exposure of internal database structures, we decouple the API from the database schema, allowing us to modify the internal structures without breaking existing client applications.
Consider the following database/internal structures as an example data structure exposed by APIs.
Alert {
string id
repeated Violation violations
string policy_name
string policy_description
string policy_enforcement
}
Changing the above Alert data structure to the following structure may break the client application.
Alert {
string id;
repeated Violation violations
Policy {
string policy_name
string policy_description
string policy_enforcement
}
}
Keeping the API focused and concise simplifies maintenance efforts and reduces the chances of introducing bugs. A clean and manageable codebase improves the overall maintainability and stability of the API.
The service name must be unique and use a noun that generally refers to a resource or product component and
must end with Service e.g. DeploymentService, ReportService, ComplianceService. Intuitive and well-known
short forms or abbreviations may be used in some cases (and could even be preferable) for succinctness
e.g. ReportConfigService, RbacService.
All gRPC methods grouped into a single service must generally pertain to the primary resource of the service.
Methods should be named such that they provide insights into the functionality.
Let us look at a few examples.StartComplianceScan, RunComplianceScan, and GetComplianceScan are not the same.
StartComplianceScanshould return without waiting for the compliance scan to complete.RunComplianceScanis ambiguous because it is unclear if the call waits for the scan to complete. The ambiguity can be removed by adding a field to the request that helps clarify the expectation e.g.bool wait_for_scan_completionif set totrueinforms the method to wait for the compliance scan to complete. However, for long-running processes, it is recommended to create a job that finishes the process asynchronously and return the job ID to the users which can be tracked via dedicated job tracking method.GetComplianceScanshould not run a compliance scan but only fetches a stored one.
Typically, the method name should follow the VerbNoun convention.
| Verb | Noun | Method name |
|---|---|---|
| List | Deployment | ListDeployments |
| Get | Deployment | GetDeployment |
| Update | Deployment | UpdateDeployment |
| Delete | Deployment | DeleteDeployment |
| Notify | Violation | NotifyViolation |
| Run | ComplianceScan | RunComplianceScan |
It is recommended that the verbs be imperative instead of inquisitive. Generally, the noun should be the resource type.
In some cases, the noun portion could be composed of multiple nouns e.g. GetVulnerabilityDeferralState, RunPolicyScan.
| Inquisitive | Imperative |
|---|---|
IsRunComplete |
GetRunStatus |
IsAdmin |
GetUserRole |
IsVulnerabilityDeferred |
GetVulnerabilityDeferralState |
The noun portion of methods that act on a single resource must be singular e.g. GetDeployment. Those methods that
act on the collection of resources must be plural e.g. ListDeployments, DeleteDeployments. Avoid prepositions
(e.g. for, by) in method names as much as possible. Typically, this can be addressed by using a distinct verb,
adding a field to the request message, or restructuring VerbNoun.
| Instead of | Use |
|
|
|
|
|
|
|
|
|
|
The following example demonstrates design if that concept of baselines could apply to multiple resource types. |
The request and response messages must be named after method names with suffix Request and Response unless
the request/response type is an empty message. Generally, resource type as response message should be avoided
e.g. use GetDeploymentResponse response instead of Deployment. This allows augmenting the response with
supplemental information in the future.
| Verb | Noun | Method name | Request message | Response message |
|---|---|---|---|---|
| List | Deployment | ListDeployments |
ListDeploymentRequest |
ListDeploymentResponse |
| Get | Deployment | GetDeployment |
GetDeploymentRequest |
GetDeploymentResponse |
| Update | Deployment | UpdateDeployment |
UpdateDeploymentRequest |
UpdateDeploymentResponse |
| Delete | Deployment | DeleteDeployment |
DeleteDeploymentRequest |
google.protobuf.Empty |
| Get | ReportStatus | GetReportStatus |
GetReportStatusRequest |
GetReportStatusResponse |
| Run | ComplianceScan | RunComplianceScan |
RunComplianceScanRequest |
RunComplianceScanResponse |
Avoid prepositions as much as possible (e.g. “for”, “with”; DeploymentWithProcessInfo, DeploymentWithImageScan).
In case such a need arises, add a field to the request message and response message.
| Instead of | Use |
|
|
|
|
|
or, |
|
|
|
All fields in the message must be lowercase and underscore separated names. The JSON names for the fields are autogenerated by the proto compiler. By default, field names are converted to camel case notation.
| Proto field name | JSON field name |
|---|---|
network_data_start_time |
networkDataStartTime |
expiry_date |
expiryDate |
Be explicit about conveying the specific purpose of fields e.g. instead of expires_on use expiry_date(/timestamp)
as it informs users if the field returns the date portion of the timestamp or the full timestamp, and use
network_data_start_time instead of network_data_since for a similar reason. The fields should convey their purpose without
requiring users to read the documentation.
This section goes over key URL guidelines that could help avoid common mistakes when building APIs.
All APIs, except custom HTTP routes, must be prefixed with API version. Custom routes should not be
version prefixed due to current design limitation of we handle gRPC vs HTTP-only endpoints (which may be mitigated
in the future). The version is typically followed by the plural form of resource noun in the service name
e.g. /v1/deployments, /v2/violations. The resource noun may be singular if it refers to non-acting resources
encapsulating acting resources e.g. /v1/compliance, /v1/networkgraph, /v1/debug, /v1/auth.
Path parameters are variable components of a URL path. They are typically used to point to a specific resource.
A URL can have several path parameters, each denoted with curly braces { }. If the request URL contains
one or more path parameters, the path parameter should clearly indicate the resource type otherwise by default
they are associated with resource type in the preceding URL component.
|
|
Acts on a specific compliance profile |
|
|
Acts on a specific network policy. |
|
|
Not recommended. Consider query string parameter pattern instead. |
Keep it simple and descriptive; avoid long-worded URL components. If one object can contain another object, design the endpoint to reflect that regardless of whether the data is structured like this in the database. If the URL describes the action, nest the action within the resource. Avoid stop words (the, and, or, of, a, an, to, for, etc.) in a URL to make it shorter and more readable.
| Instead of | Use | |
|---|---|---|
GET: /v1/depoymentsbynamespace |
GET: /v1/namespaces/{namespace}/deployments |
Get all deployments in a specific namespace |
GET: /v1/reportsmetadata/{id} |
GET: /v1/reports/jobs/{id}/metadata |
Get metadata of report job |
GET: /v1/reports/status/{id} |
GET: /v1/reports/jobs/{id}/status |
Get status of report job |
GET: /v1/complianceprofiles |
GET: /v1/compliance/profiles |
Get all compliance profiles |
POST: /v1/resetbaselineforcluster/{cluster_id} |
POST: /v1/baselines/cluster/{cluster_id}/reset |
Reset baseline for a specific cluster |
If splitting the words into multiple URL components is not intuitive, it is recommended to use a hyphen(-).
| Instead of | Use |
|---|---|
/v1/kernelsupport |
/v1/kernel-support |
/v1/kernel_support |
/v1/kernel-support |
/v1/securitypolicy |
/v1/security-policy |
If the API acts on specific attribute of the resource, using URL query parameters is more intuitive over long-worded or hierarchical identifiers.
| Instead of | Use |
|---|---|
/v1/deferredcves |
/v1/cves?deferred=true |
/v1/inactiveviolations |
/v1/violations?inactive=true |
/v1/fixablecves |
/v1/cves?fixable=true |
/v1/runningreports |
/v1/reports?status=running |
URLs should not be duplicated. A request URL and request method should uniquely identify an API. For example,
GET: /v1/deployments/{id} conflicts with GET: /v1/deployments/violations.
A GET API must not declare a body but instead specify the response criteria using path and query parameters,
therefore, the gRPC request message fields should map to the URL path or query parameters.
The parameters could be used for identifying, filtering, sorting, paginating, tracking the source, translation, etc.
POST requests to create a resource must not accept the resource ID. IDs must be generated on the backend.
Only POST requests to perform an action on a specific resource should accept a resource ID.
A GET API must use an HTTP GET verb. For example, the following API configuration is not recommended and
should be avoided:
rpc GetExistingProbes(GetExistingProbesRequest) returns (GetExistingProbesResponse) {
option (google.api.http) = {
post: "/v1/probeupload/getexisting"
};
}
While we generally try to avoid deprecations, it is sometimes necessary.
This section only applies for General Available (GA) features. For Technology Preview, these guidelines do not apply and only need to be announced in release notes.
Before deprecating APIs, clarify with relevant stakeholders (e.g. product team, solution engineering) the deprecation. In addition, if available for your API, the analytics we collect may also be used to gauge whether deprecation is an option or not.
While there are no strict guidelines from Red Hat about announcing deprecations, historically we have announced deprecations two releases in advance. This gives users enough time to adjust potential usages of the deprecated API.
The deprecation announcement must be done at least within the release notes within the Deprecated Features
section and the associated service proto (i.e. the API documentation).
In addition, there are other forms of announcement that have been used historically. Use these with your best judgement:
- Within the UI
- This can be a banner or other hints announcing the deprecation close to places where the respective API is being used.
- As an example, the SAC resource consolidation efforts led to a banner being created in the UI where resources were being used.
- Within Central logs
- This can be done during Central startup or during API calls, informing the user that the API service will be removed in the future.
- Be aware that high-cardinality APIs are not an ideal candidate for logging per API call.
Once the deprecation time has been met after two releases, the API can safely be fully removed (i.e. removing the service associated with the API), but it is up to your discretion to postpone or not to go through with the deprecation.