Skip to content

feat: Add support for Entra Application#5232

Open
theunrepentantgeek wants to merge 22 commits into
mainfrom
feature/entra-application
Open

feat: Add support for Entra Application#5232
theunrepentantgeek wants to merge 22 commits into
mainfrom
feature/entra-application

Conversation

@theunrepentantgeek

@theunrepentantgeek theunrepentantgeek commented Mar 12, 2026

Copy link
Copy Markdown
Member

What this PR does

Adds support for Entra Application as a new resource type.

How does this PR make you feel?

gif

Fixes #5217

@theunrepentantgeek theunrepentantgeek left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Deep Adversarial Code Review

Three independent review perspectives (Advocate, Skeptic, Architect) analyzed this PR. Findings synthesized below, posted as inline comments at relevant locations.

Priority Count Summary
🔴 Critical 1 Nil pointer panic on external deletion
🟠 High 4 OData injection, can't clear fields, FEATURE.md, dead field
🟡 Medium 3 Status from pre-PATCH, missing configmap save, DisplayName validation
🔵 Low 3 Extract shared reconciler, decouple SDK, cosmetic consistency

Verified robust: Annotation persistence on partial create failure ✅, Delete path ✅, Webhook defaulting ✅

Comment thread v2/internal/reconcilers/entra/entra_application_reconciler.go
Comment thread v2/internal/reconcilers/entra/entra_application_reconciler.go Outdated
Comment thread v2/api/entra/v1/application_types.go Outdated
Comment thread FEATURE.md Outdated
Comment thread v2/internal/reconcilers/entra/entra_application_reconciler.go Outdated
Comment thread v2/internal/reconcilers/entra/entra_application_reconciler.go
Comment thread v2/api/entra/v1/application_types.go
Comment thread v2/internal/reconcilers/entra/entra_application_reconciler.go
Comment thread v2/api/entra/v1/application_types.go
Comment thread v2/internal/reconcilers/entra/entra_application_reconciler.go Outdated
theunrepentantgeek and others added 19 commits June 11, 2026 13:11
The FEATURE.md file is an implementation plan that belongs in the PR
description or issue, not in the repository root alongside README.md
and CONTRIBUTING.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The CredentialProvider field is declared on both EntraApplicationReconciler
and EntraSecurityGroupReconciler but never populated by their constructors
and never referenced anywhere. The credential flow goes through
EntraClientFactory → EntraClientCache.GetConnection() → the provider,
making this field unnecessary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace reconcile.Result{} with ctrl.Result{} in both the Application
and SecurityGroup reconcilers for consistency. They are the same
underlying type, but all other return sites use ctrl.Result{}.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract the duplicated isNotFound method from both
EntraApplicationReconciler and EntraSecurityGroupReconciler into a
shared package-level function in errors.go.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
loadApplicationByID returns (nil, nil) for 404 errors, not an error.
The isNotFound(err) check was dead code since err is always nil for a
404. The nil Applicationable would reach AssignToApplication and panic.

Fix by checking for nil result after the error check instead of
checking isNotFound on the error. Apply same fix to SecurityGroup
reconciler which has the same pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Display names containing single quotes (e.g., "O'Brien's App") produce
malformed OData filters, causing 400 errors from the Graph API. Escape
single quotes by doubling them per OData conventions.

Fix applied to both Application and SecurityGroup reconcilers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The PATCH return value was discarded and status was populated from the
locally-mutated model. If the Graph API normalizes, rejects, or
augments any field, status would diverge from reality.

Now use the server response from PATCH to populate status, ensuring
it reflects the authoritative server state. Applied to both
Application and SecurityGroup reconcilers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The create() path saves ConfigMaps but update() did not, meaning
ConfigMaps would not be refreshed after an update until the next
UpdateStatus() cycle. Add the call for symmetry with create().

Applied to both Application and SecurityGroup reconcilers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously, nil/empty guards meant that removing a field from the spec
(e.g., clearing identifierUris, tags, web, spa, or publicClient) would
never send the empty value to the API - the field would persist
unchanged in Entra.

Now all fields are always set so that clearing a value in the K8s spec
correctly propagates to the Graph API. Same fix applied to
sub-type assignment methods (Web, Spa, PublicClient).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Comment thread v2/azure-arm/entra.yaml Outdated
@theunrepentantgeek theunrepentantgeek marked this pull request as ready for review June 16, 2026 03:16
Copilot AI review requested due to automatic review settings June 16, 2026 03:16

Copilot AI left a comment

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.

Pull request overview

Adds a new hand-written Entra (Azure AD) Application resource to ASO v2 (non-ARM), including controller reconciliation via Microsoft Graph, webhooks, samples, docs, and record/replay tests to validate CRUD behavior.

Changes:

  • Introduces entra.azure.com/v1 Application API type, webhook defaulting/validation, and controller registration.
  • Implements a Microsoft Graph-based reconciler for Applications (adopt/create/update/delete) and updates SecurityGroup reconciler behavior.
  • Adds samples, docs reference entries, and test recordings + a new CRUD test for Entra Applications.

Reviewed changes

Copilot reviewed 15 out of 17 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
v2/tools/generator/internal/astbuilder/comments.go Minor linter directive formatting adjustment.
v2/samples/entra/v1/v1_application.yaml New sample manifest for Entra Application.
v2/internal/testsamples/recordings/Test_Samples_CreationAndDeletion/Test_Entra_v1_CreationAndDeletion.yaml Updated sample-test VCR recording to include Application interactions.
v2/internal/reconcilers/entra/odata.go Adds OData string escaping helper for Graph filters.
v2/internal/reconcilers/entra/errors.go Adds shared Graph “not found” detection helper.
v2/internal/reconcilers/entra/entra_securitygroup_reconciler.go Refactors not-found handling + adds status/resource export behavior changes.
v2/internal/reconcilers/entra/entra_application_reconciler.go New reconciler implementing CRUD/adoption for Entra Applications + configmap export.
v2/internal/controllers/recordings/Test_Entra_SecurityGroup_v1_CRUD.yaml Updated SecurityGroup controller VCR recording.
v2/internal/controllers/recordings/Test_Entra_Application_v1_CRUD.yaml New Application controller VCR recording.
v2/internal/controllers/entra_application_v1_test.go New controller CRUD test for Entra Application + configmap assertions.
v2/internal/controllers/controller_resources.go Registers Application storage type and webhook.
v2/azure-arm/entra.yaml Marks Application as supported from v2.20.0 in Entra group config.
v2/api/entra/v1/zz_generated.deepcopy.go Generated deepcopy updates for new Application types.
v2/api/entra/v1/webhook/application_webhook_types.go Adds defaulting + validator webhook for Application.
v2/api/entra/v1/application_types.go Defines Application spec/status and Graph model assignment helpers.
docs/hugo/content/reference/entra/_index.md Documents Application availability and links to sample.
docs/hugo/content/reference/_index.md Adds Application to the global reference index.
Files not reviewed (1)
  • v2/api/entra/v1/zz_generated.deepcopy.go: Generated file

- chunked
content_length: -1
uncompressed: true
body: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#applications/$entity","id":"12bffd98-e5b5-4a9e-a8c1-25ac9b532f27","deletedDateTime":null,"appId":"e9cd4cdd-db0c-4d5e-a4f1-3b189514f799","applicationTemplateId":null,"disabledByMicrosoftStatus":null,"createdByAppId":"04b07795-8ddb-461a-bbee-02f9e1bf7b46","createdDateTime":"2001-02-03T04:05:06Z","displayName":"ASO Test Application","description":null,"groupMembershipClaims":null,"identifierUris":[],"isDeviceOnlyAuthSupported":null,"isDisabled":null,"isFallbackPublicClient":null,"nativeAuthenticationApisEnabled":null,"notes":null,"publisherDomain":"bevanarpsoutlook.onmicrosoft.com","serviceManagementReference":null,"signInAudience":"AzureADMyOrg","tags":[],"tokenEncryptionKeyId":null,"uniqueName":null,"samlMetadataUrl":null,"defaultRedirectUri":null,"certification":null,"optionalClaims":null,"servicePrincipalLockConfiguration":null,"requestSignatureVerification":null,"addIns":[],"api":{"acceptMappedClaims":null,"knownClientApplications":[],"requestedAccessTokenVersion":null,"oauth2PermissionScopes":[],"preAuthorizedApplications":[]},"appRoles":[],"info":{"logoUrl":null,"marketingUrl":null,"privacyStatementUrl":null,"supportUrl":null,"termsOfServiceUrl":null},"keyCredentials":[],"parentalControlSettings":{"countriesBlockedForMinors":[],"legalAgeGroupRule":"Allow"},"passwordCredentials":[],"publicClient":{"redirectUris":[]},"requiredResourceAccess":[],"verifiedPublisher":{"displayName":null,"verifiedPublisherId":null,"addedDateTime":null},"web":{"homePageUrl":null,"logoutUrl":null,"redirectUris":[],"implicitGrantSettings":{"enableAccessTokenIssuance":false,"enableIdTokenIssuance":false},"redirectUriSettings":[]},"spa":{"redirectUris":[]}}'
Content-Type:
- application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8
Location:
- https://graph.microsoft.com/v1.0/9b0c84c7-21c4-4c92-ae62-d9c1eb53333f/directoryObjects/12bffd98-e5b5-4a9e-a8c1-25ac9b532f27/Microsoft.DirectoryServices.Application
Comment on lines +68 to +71
app, err := r.asApplication(obj)
if err != nil {
return ctrl.Result{}, eris.Wrapf(err, "creating or updating application %s", app.Name)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should probably be fixed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Applies throughout

Comment on lines +109 to +112
app, err := r.asApplication(obj)
if err != nil {
return ctrl.Result{}, eris.Wrapf(err, "deleting application %s", app.Name)
}
Comment on lines +340 to +343
app, err := r.asApplication(obj)
if err != nil {
return eris.Wrapf(err, "updating status of application %s", app.Name)
}
Comment on lines +203 to +211
if result == nil {
// Didn't get a result back from the patch, load the group again to get the latest state
log.V(Status).Info("No result returned from update, reloading group to get latest state")

result, err = r.loadGroupByID(ctx, id, client.Client())
if err != nil {
return ctrl.Result{}, eris.Wrapf(err, "getting group by ID %s after update returned no result", id)
}
}
Comment on lines 349 to 353
groupable, err := r.loadGroupByID(ctx, id, client.Client())
if err != nil {
// If the group doesn't exist, nothing to do as we're probably in the midst of deleting it
if r.isNotFound(err) {
if isNotFound(err) {
return nil
@theunrepentantgeek theunrepentantgeek added this to the v2.20.0 milestone Jun 18, 2026
model.SetGroupMembershipClaims(spec.GroupMembershipClaims)
}

type ApplicationStatus struct {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How did you decide what to put in status, versus what not to?

}

func (status *ApplicationStatus) AssignFromApplication(model models.Applicationable) {
if model != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

minor, style:

you could avoid nesting by bailing out early if model != nil

if model != nil {
    return
}

Comment on lines +68 to +71
app, err := r.asApplication(obj)
if err != nil {
return ctrl.Result{}, eris.Wrapf(err, "creating or updating application %s", app.Name)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should probably be fixed

)

// EntraSecurityGroupReconciler reconciles an Entra security group.
// TODO: Factor out common code shared with other Entra resources into entraGenericReconciler

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this something that makes sense to do as part of this PR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comment on lines +68 to +71
app, err := r.asApplication(obj)
if err != nil {
return ctrl.Result{}, eris.Wrapf(err, "creating or updating application %s", app.Name)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Applies throughout

}

var displayName string
if app.Spec.DisplayName != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

displayName = to.FromPtr() would work here, though see comment below I dont even think you need/want this naem at all?

}

if result == nil {
// Didn't get a result back from the patch, load the application again to get the latest state

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you expand... is this expected? I don't fully understand what "no response from patch" means in terms of if it was expected or what ends up happening (which is that we load it and then return?)

// Try to find the application by DisplayName
displayName := app.Spec.DisplayName
if to.Value(displayName) == "" {
// Can't adopt without a display name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this an error condition? Or success?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems unexpected, should this be an error?


// saveAssociatedKubernetesResources retrieves Kubernetes resources to create and saves them to Kubernetes.
// If there are no resources to save this method is a no-op.
func (r *EntraApplicationReconciler) saveAssociatedKubernetesResources(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Duplicate from EntraSecurityGroupReconciler? Wondering if we can combine some of this w/ like an interface that exposes operatorSpec.ConfigMaps? Similar to what we do with the generic reconciler?

return ctrl.Result{}, eris.Wrapf(err, "getting group by ID %s", id)
}

if g == nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

minor: why move this?

@theunrepentantgeek theunrepentantgeek changed the title Add support for Entra Application feat: Add support for Entra Application Jun 24, 2026
@theunrepentantgeek theunrepentantgeek modified the milestones: v2.20.0, v2.21.0 Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

Feature: Add support for Entra Applications

3 participants