Skip to content

Plan: AEP Compliance Gap Analysis & Remediation#72

Closed
ebichman-1 wants to merge 1 commit into
dcm-project:mainfrom
ebichman-1:aep-rules-gap-analysis
Closed

Plan: AEP Compliance Gap Analysis & Remediation#72
ebichman-1 wants to merge 1 commit into
dcm-project:mainfrom
ebichman-1:aep-rules-gap-analysis

Conversation

@ebichman-1
Copy link
Copy Markdown

Plan: AEP Compliance Gap Analysis & Remediation

Context

Requests an analysis of the
Makefile and .spectral.yaml against AEP compliance standards
(https://aep.dev/5/), identifying missing linting rules, misconfigured
enforcement, and gaps in automation. The main openapi.yaml currently passes
all 55 AEP linter rules, but there are gaps in tooling coverage, custom rule
enforcement, and standard method completeness per AEP-5.

Gap Analysis Summary

 ┌─────┬───────────────────────────────────────────┬──────────┬────────────┐
 │  #  │                    Gap                    │ Severity │  Category  │
 ├─────┼───────────────────────────────────────────┼──────────┼────────────┤
 │ 1   │ Makefile check-aep skips servicetype      │ High     │ Tooling    │
 │     │ specs                                     │          │            │
 ├─────┼───────────────────────────────────────────┼──────────┼────────────┤
 │ 2   │ Servicetype specs fail with non-AEP       │ Medium   │ Tooling    │
 │     │ warnings                                  │          │            │
 ├─────┼───────────────────────────────────────────┼──────────┼────────────┤
 │ 3   │ No custom rule for AEP-148 standard       │ Medium   │ Linting    │
 │     │ fields                                    │          │            │
 ├─────┼───────────────────────────────────────────┼──────────┼────────────┤
 │ 4   │ Missing Update/Delete for ServiceType     │ High     │ Spec +     │
 │     │ (AEP-5)                                   │          │ Code       │
 ├─────┼───────────────────────────────────────────┼──────────┼────────────┤
 │ 5   │ Missing Update for CatalogItemInstance    │ High     │ Spec +     │
 │     │ (AEP-5)                                   │          │ Code       │
 ├─────┼───────────────────────────────────────────┼──────────┼────────────┤
 │ 6   │ Error schema references RFC 7807 instead  │ Low      │ Spec       │
 │     │ of RFC 9457                               │          │            │
 └─────┴───────────────────────────────────────────┴──────────┴────────────┘

Phase 1: Tooling Fixes (.spectral.yaml + Makefile)

Step 1.1 — Expand Makefile check-aep target

File: Makefile:81-82

Add servicetype specs to the lint command so developers can catch violations
locally:

 check-aep:
        spectral lint --fail-severity=warn ./api/v1alpha1/openapi.yaml
 ./api/v1alpha1/servicetypes/*/spec.yaml

Step 1.2 — Add overrides for servicetype specs

File: .spectral.yaml

Servicetype specs are type-only schemas (no paths, no servers). The general
OpenAPI rules oas3-api-servers, info-contact, and oas3-unused-component are
inapplicable and should be suppressed:

 overrides:
   - files:
       - "api/v1alpha1/servicetypes/*/spec.yaml"
     rules:
       oas3-api-servers: "off"
       info-contact: "off"
       oas3-unused-component: "off"

Step 1.3 — Add custom AEP-148 standard fields rule

File: .spectral.yaml

Add a rule enforcing that resources with x-aep-resource include the standard
fields path, create_time, update_time:

 aep-148-resource-standard-fields:
   description: AEP resources should have standard fields (path, create_time,
 update_time)
   message: "Resource with x-aep-resource should include '{{property}}' field
 per AEP-148"
   severity: warn
   given: $.components.schemas[?(@.x-aep-resource &&
 !@.x-aep-resource.singleton)].properties
   then:
     - field: path
       function: truthy
     - field: create_time
       function: truthy
     - field: update_time
       function: truthy

Phase 2: OpenAPI Spec — Add Missing Standard Methods (AEP-5)

AEP-5 states: "Ideally all five standard methods should be exposed for every
resource."

Step 2.1 — Add ServiceType Update (PATCH) endpoint

File: api/v1alpha1/openapi.yaml — add under /service-types/{serviceTypeId}
path

Following the existing updateCatalogItem pattern (lines 308-350):

  • Method: PATCH
  • operationId: updateServiceType
  • Content-Type: application/merge-patch+json
  • Request body: $ref: '#/components/schemas/ServiceType'
  • Response 200: returns updated ServiceType
  • Error responses: 400, 401, 403, 404, 500

Step 2.2 — Add ServiceType Delete (DELETE) endpoint

File: api/v1alpha1/openapi.yaml — add under /service-types/{serviceTypeId}
path

Following the existing deleteCatalogItem pattern (lines 352-377):

  • Method: DELETE
  • operationId: deleteServiceType
  • Response 204: no body
  • Error responses: 401, 403, 404, 409 (has catalog items), 500
  • Business rule: prevent deletion if CatalogItems reference this ServiceType

Step 2.3 — Add CatalogItemInstance Update (PATCH) endpoint

File: api/v1alpha1/openapi.yaml — add under
/catalog-item-instances/{catalogItemInstanceId} path

Following the existing updateCatalogItem pattern:

  • Method: PATCH
  • operationId: updateCatalogItemInstance
  • Content-Type: application/merge-patch+json
  • Request body: $ref: '#/components/schemas/CatalogItemInstance'
  • Response 200: returns updated CatalogItemInstance
  • Immutable fields: api_version, spec.catalog_item_id
  • Error responses: 400, 401, 403, 404, 500

Step 2.4 — Fix Error schema RFC reference

File: api/v1alpha1/openapi.yaml:1060

Change RFC 7807 to RFC 9457 in the Error schema description.

Phase 3: Regenerate Code

Step 3.1 — Regenerate all API code

Run make generate-api to regenerate:

  • api/v1alpha1/types.gen.go — new request/response types
  • api/v1alpha1/spec.gen.go — embedded spec
  • internal/api/server/server.gen.go — new StrictServerInterface methods
  • pkg/client/client.gen.go — new client methods

Phase 4: Implement Missing Handlers

Step 4.1 — ServiceType Store — Add Update and Delete methods

File: internal/store/service_type.go

Following the catalogItemStore patterns in internal/store/catalog_item.go:

  • Update(ctx, *model.ServiceType) error — update mutable fields only
  • Delete(ctx, id string) error — check for referencing CatalogItems before
    deleting

Add corresponding sentinel errors: ErrServiceTypeHasCatalogItems

Step 4.2 — ServiceType Service — Add Update and Delete methods

File: internal/service/service_type.go

Following the catalogItemService patterns in internal/service/catalog_item.go:

  • Update(ctx, id, *UpdateServiceTypeRequest) (*v1alpha1.ServiceType, error)
  • Delete(ctx, id string) error

Step 4.3 — ServiceType Handler — Add UpdateServiceType and DeleteServiceType

File: internal/handlers/v1alpha1/service_type.go

Following the handler patterns in internal/handlers/v1alpha1/catalog_item.go:

  • Implement UpdateServiceType mapping HTTP request to service call
  • Implement DeleteServiceType returning 204

Add error mapping functions in the service_type errors file.

Step 4.4 — CatalogItemInstance Handler/Service — Add UpdateCatalogItemInstance

Files:

  • internal/service/catalog_item_instance.go — add Update method
  • internal/handlers/v1alpha1/catalog_item_instance.go — add handler

Note: The store layer already has Update() and UpdateResourceID() methods, so
only handler and service implementations are needed.

Phase 5: Tests

Step 5.1 — Add unit tests for new handlers

Follow existing test patterns in internal/handlers/v1alpha1/*_test.go

Step 5.2 — Add unit tests for new service methods

Follow existing test patterns in internal/service/*_test.go

Step 5.3 — Add unit tests for new store methods

Follow existing test patterns in internal/store/*_test.go


Verification

  1. make check-aep — all specs pass with 0 errors/warnings
  2. make generate-api && make check-generate-api — generated files in sync
  3. make test — all tests pass
  4. make build — binary builds successfully

Files to Modify

 ┌────────────────────────────────────────────────┬────────────────────────┐
 │                      File                      │        Changes         │
 ├────────────────────────────────────────────────┼────────────────────────┤
 │ .spectral.yaml                                 │ Add overrides, add     │
 │                                                │ AEP-148 custom rule    │
 ├────────────────────────────────────────────────┼────────────────────────┤
 │                                                │ Expand check-aep to    │
 │ Makefile                                       │ include servicetype    │
 │                                                │ specs                  │
 ├────────────────────────────────────────────────┼────────────────────────┤
 │ api/v1alpha1/openapi.yaml                      │ Add 3 endpoints, fix   │
 │                                                │ RFC reference          │
 ├────────────────────────────────────────────────┼────────────────────────┤
 │ internal/store/service_type.go                 │ Add Update, Delete     │
 │                                                │ methods                │
 ├────────────────────────────────────────────────┼────────────────────────┤
 │                                                │ Add Update, Delete     │
 │ internal/service/service_type.go               │ methods + request      │
 │                                                │ types                  │
 ├────────────────────────────────────────────────┼────────────────────────┤
 │                                                │ Add UpdateServiceType, │
 │ internal/handlers/v1alpha1/service_type.go     │  DeleteServiceType     │
 │                                                │ handlers               │
 ├────────────────────────────────────────────────┼────────────────────────┤
 │ internal/service/catalog_item_instance.go      │ Add Update method +    │
 │                                                │ request type           │
 ├────────────────────────────────────────────────┼────────────────────────┤
 ├────────────────────────────────────────────────┼────────────────────────┤
 │ internal/service/catalog_item_instance.go      │ Add Update method +    │
 │                                                │ request type           │
 ├────────────────────────────────────────────────┼────────────────────────┤
 │ internal/handlers/v1alpha1/catalog_item_instan │ Add UpdateCatalogItemI │
 │ ce.go                                          │ nstance handler        │
 ├────────────────────────────────────────────────┼────────────────────────┤
 │                                                │ types.gen.go,          │
 │ Generated files (via make generate-api)        │ spec.gen.go,           │
 │                                                │ server.gen.go,         │
 │                                                │ client.gen.go          │
 └────────────────────────────────────────────────┴────────────────────────┘

…ting gaps

 - Add Update (PATCH) and Delete (DELETE) endpoints for ServiceType.
 - Update (PATCH) for CatalogItemInstance to satisfy AEP-5 standard method
 requirements.
 - Expand Makefile check-aep target to lint servicetype specs,
 - Add .spectral.yaml overrides for type-only schemas, add custom AEP-148
 standard fields rule,
 - Update Error schema RFC reference from 7807 to
 9457.

 Implement corresponding store, service, and handler layers with tests.
 Regenerate all API code (types, spec, server, client).

 Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
 Signed-off-by: ebichman-1 <ebichman@redhat.com>

Signed-off-by: ebichman-1 <ebichman@redhat.com>
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In UpdateServiceType, the handler builds UpdateServiceTypeRequest as Metadata: request.Body.Metadata and Spec: request.Body.Spec, but request.Body.Metadata is *v1alpha1.ServiceTypeMetadata while the request struct expects *struct{ Labels *map[string]string }, which will not compile; you likely need an explicit translation layer that copies Metadata.Labels into the pointer field.
  • The UpdateCatalogItemInstance handler always sets DisplayName and Spec in UpdateCatalogItemInstanceRequest (taking addresses of the body fields), so a PATCH with omitted fields will still overwrite existing values with zero values; if you want true merge-patch semantics, consider distinguishing between omitted and explicitly-null/empty fields (e.g., by using pointer fields in the generated type or by inspecting the raw JSON).
  • The ServiceTypeStore.Delete implementation infers ErrServiceTypeHasCatalogItems by string-matching "foreign key" in the DB error, which is brittle across drivers and schema changes; if possible, rely on specific DB error codes or a well-defined constraint name to make this mapping more robust.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `UpdateServiceType`, the handler builds `UpdateServiceTypeRequest` as `Metadata: request.Body.Metadata` and `Spec: request.Body.Spec`, but `request.Body.Metadata` is `*v1alpha1.ServiceTypeMetadata` while the request struct expects `*struct{ Labels *map[string]string }`, which will not compile; you likely need an explicit translation layer that copies `Metadata.Labels` into the pointer field.
- The `UpdateCatalogItemInstance` handler always sets `DisplayName` and `Spec` in `UpdateCatalogItemInstanceRequest` (taking addresses of the body fields), so a PATCH with omitted fields will still overwrite existing values with zero values; if you want true merge-patch semantics, consider distinguishing between omitted and explicitly-null/empty fields (e.g., by using pointer fields in the generated type or by inspecting the raw JSON).
- The `ServiceTypeStore.Delete` implementation infers `ErrServiceTypeHasCatalogItems` by string-matching `"foreign key"` in the DB error, which is brittle across drivers and schema changes; if possible, rely on specific DB error codes or a well-defined constraint name to make this mapping more robust.

## Individual Comments

### Comment 1
<location path="internal/handlers/v1alpha1/catalog_item_instance.go" line_range="120-122" />
<code_context>
+func (h *Handler) UpdateCatalogItemInstance(ctx context.Context, request server.UpdateCatalogItemInstanceRequestObject) (server.UpdateCatalogItemInstanceResponseObject, error) {
+	h.logger.InfoContext(ctx, "Updating catalog item instance", "id", request.CatalogItemInstanceId)
+
+	updateReq := &service.UpdateCatalogItemInstanceRequest{
+		DisplayName: &request.Body.DisplayName,
+		Spec:        &request.Body.Spec,
+	}
+
</code_context>
<issue_to_address>
**issue (bug_risk):** UpdateCatalogItemInstance treats an omitted display_name in the merge-patch body as an explicit empty string, which breaks JSON Merge Patch semantics.

Because `request.Body.DisplayName` is a plain string, it always deserializes to `""` when omitted, and the handler unconditionally passes `&request.Body.DisplayName` into `UpdateCatalogItemInstanceRequest`. As a result, `mergeCatalogItemInstance` will clear the existing display name even when the client did not include `display_name` in the patch, breaking JSON Merge Patch semantics (omitted fields should be ignored).

To fix this, you’ll need a way to distinguish omission from an explicit update, e.g. by:
- Using `*string` for `display_name` in the generated PATCH body type, or
- Adding a layer that inspects the raw body to see if `display_name` was present before setting it on the service request, or
- Introducing a dedicated patch DTO with pointer fields for patchable attributes.

Without this, PATCH calls can unintentionally blank out display names.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +120 to +122
updateReq := &service.UpdateCatalogItemInstanceRequest{
DisplayName: &request.Body.DisplayName,
Spec: &request.Body.Spec,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): UpdateCatalogItemInstance treats an omitted display_name in the merge-patch body as an explicit empty string, which breaks JSON Merge Patch semantics.

Because request.Body.DisplayName is a plain string, it always deserializes to "" when omitted, and the handler unconditionally passes &request.Body.DisplayName into UpdateCatalogItemInstanceRequest. As a result, mergeCatalogItemInstance will clear the existing display name even when the client did not include display_name in the patch, breaking JSON Merge Patch semantics (omitted fields should be ignored).

To fix this, you’ll need a way to distinguish omission from an explicit update, e.g. by:

  • Using *string for display_name in the generated PATCH body type, or
  • Adding a layer that inspects the raw body to see if display_name was present before setting it on the service request, or
  • Introducing a dedicated patch DTO with pointer fields for patchable attributes.

Without this, PATCH calls can unintentionally blank out display names.

@gciavarrini
Copy link
Copy Markdown
Contributor

This PR requires an analysis first.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants