Skip to content

refactor(configure): replace az/gcloud CLI shell-outs with native SDK calls#1279

Open
cristim wants to merge 5 commits into
mainfrom
refactor/az-cli-to-sdk
Open

refactor(configure): replace az/gcloud CLI shell-outs with native SDK calls#1279
cristim wants to merge 5 commits into
mainfrom
refactor/az-cli-to-sdk

Conversation

@cristim

@cristim cristim commented Jun 24, 2026

Copy link
Copy Markdown
Member

Summary

Replace exec.Command shell-outs to cloud CLIs with native SDK calls. After the owner-requested follow-up, all reducible cloud operations now go through the SDK and fail loud on error -- no silent CLI fallbacks remain. Only three genuinely irreducible CLI calls are kept (interactive auth bootstrap + local gcloud state).

Replaced calls

Azure - CI sanity test (programmatic/CI path)

ci_cd_sanity_tests/pkg/sanity/azure/azure.go:

  • az account set -> armsubscriptions.Client.Get (verify subscription reachable)
  • az account show -o json -> armsubscriptions.Client.Get (subscription/tenant identity, re-encoded as the same JSON shape so validateAccountExpectations is unchanged)
  • az group list -> armresources.ResourceGroupsClient.NewListPager
  • az vm list -> armcompute.VirtualMachinesClient.NewListAllPager

Azure - configure wizard

cmd/configure_azure.go:

  • az account list --output table -> armsubscriptions.Client.NewListPager (fails loud on auth error)
  • az ad sp create-for-rbac -> Microsoft Graph SDK + armauthorization/v2 (see below)

Azure service principal creation (cmd/configure_azure_sp.go)

The create-for-rbac equivalent, fully via SDK:

  1. Create application registration (name CUDly) -> Microsoft Graph Applications().Post
  2. Add password credential -> Graph Applications().ByApplicationId(...).AddPassword().Post (returns the secret text -- only available at creation, exactly like create-for-rbac)
  3. Create service principal for the app's client ID -> Graph ServicePrincipals().Post
  4. Resolve "Reservations Administrator" role definition by display name at subscription scope -> armauthorization RoleDefinitionsClient.NewListPager with a roleName eq '...' filter
  5. Assign the role binding the SP principal to that role definition at /subscriptions/<id> -> armauthorization RoleAssignmentsClient.Create

The resulting appId (client ID), generated client secret, and tenant ID are printed in the same shape az ad sp create-for-rbac prints, so the operator feeds them into the credential collection step. Tenant ID is resolved from the subscription via armsubscriptions.

A narrow azureSPProvisioner interface wraps the four cloud operations so the orchestration is unit-tested with a mock (the Graph / armauthorization concrete clients are not interfaces). Tests assert app name = CUDly, role = Reservations Administrator, scope = /subscriptions/<id>, that the secret is surfaced, and per-step error propagation (no resolve/assign after an earlier failure).

GCP - configure wizard

cmd/configure_gcp.go (all fail loud on SDK error):

  • gcloud projects list -> cloudresourcemanager/v1 Projects.List
  • gcloud iam service-accounts create -> iam/v1 Projects.ServiceAccounts.Create
  • gcloud projects add-iam-policy-binding --role roles/compute.admin -> cloudresourcemanager/v1 GetIamPolicy + SetIamPolicy
  • gcloud iam service-accounts keys create -> iam/v1 Projects.ServiceAccounts.Keys.Create (key written from base64-decoded PrivateKeyData)

Fail-loud, no silent fallback

All previous "falls back to the CLI silently" paths were removed (project rule: no silent fallbacks). On an SDK/ADC auth failure each step now returns a clear error instructing the operator to run az login / gcloud auth login / gcloud auth application-default login first.

Calls left as CLI (the only three remaining)

Each is an interactive auth bootstrap or a local-state write with no SDK equivalent, and carries a justified //nolint:gosec // G204: ...:

Call Reason
az login Interactive browser OAuth; no SDK equivalent that establishes/caches the operator credential
gcloud auth login Interactive browser OAuth; same
gcloud config set project Writes local ~/.config/gcloud state; no cloud-API equivalent

Removing the az ad sp create-for-rbac subprocess eliminates its gosec G204 finding; the three retained calls carry precise nolints. (PR #1265 nolints G204 broadly; reconciliation happens at merge time -- #1265's branch is untouched.)

Auth model confirmation

  • Azure: DefaultAzureCredential throughout. Its chain includes AzureCLICredential, so the az login session is reused. In CI it resolves via AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_CLIENT_SECRET. Graph calls use the https://graph.microsoft.com/.default scope.
  • GCP: google.DefaultTokenSource (ADC). gcloud auth login updates the user session but does NOT populate the ADC cache; the wizard documents that operators must also run gcloud auth application-default login.

New SDK deps

  • github.com/microsoftgraph/msgraph-sdk-go v1.99.0 (direct, root module) -- application + service principal + addPassword
  • github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 (direct, root module) -- role definition resolution + role assignment
  • go mod tidy pulled the kiota transitive deps and bumped azcore / azidentity / otel to satisfy msgraph requirements.
  • Earlier round (still in this PR): armcompute/v5, armsubscriptions promoted to direct; armresources v1.2.0 promoted to direct; google.golang.org/api/iam/v1 and cloudresourcemanager/v1 are sub-packages of the already-required google.golang.org/api.

Tests / verification

  • go build ./... and go test ./... pass (5,486 tests across 38 packages).
  • New SP-create unit tests pass (8 cases).
  • golangci-lint run on touched files: no new findings; the only gosec nolints are on the three irreducible CLI calls (G204 fully suppressed there). Remaining gosec G117/G115 findings are pre-existing on unchanged lines.
  • gocyclo -over 10: no touched function exceeds 10.
  • No silent fallback remains (grepped).

Summary by CodeRabbit

  • New Features
    • Updated Azure and GCP interactive setup flows to use cloud SDK credentials for subscription/project discovery and automated credential provisioning.
    • Enhanced Azure sanity checks to use SDK-based verification and provide sample resource group/VM summaries.
    • Added Azure service principal provisioning that creates the app, generates credentials, and assigns the subscription-scope Reservations Administrator role with safe rollback on failure.
  • Bug Fixes
    • Improved account/expectation validation to keep output/formatting consistent, including truncation behavior.
  • Tests
    • Added/expanded unit tests covering account JSON encoding, truncation, Azure SP provisioning (success/failure/rollback), and role-assignment retry logic.

@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy type/chore Maintenance / non-user-visible labels Jun 24, 2026
@cristim

cristim commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b35ac612-a87f-4473-a922-0469fdf8c076

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR replaces Azure and GCP setup paths with SDK-based flows, updates Azure sanity checks to use Azure SDK clients, and adds Azure service principal provisioning with rollback and retry handling. It also updates dependencies and expands tests around Azure JSON encoding and provisioning behavior.

Changes

SDK Migration

Layer / File(s) Summary
Dependency updates
go.mod
Updates Azure, Microsoft Graph, Kiota, OpenTelemetry, and related module requirements used by the SDK-backed flows.
Azure sanity checks
ci_cd_sanity_tests/pkg/sanity/azure/azure.go, ci_cd_sanity_tests/pkg/sanity/azure/azure_test.go
Adds Azure subscription identity encoding, expectation JSON adaptation, report helpers, and SDK-backed checks for subscription reachability, resource groups, and virtual machines.
Azure configure flow
cmd/configure_azure.go
Reworks the interactive Azure setup to pass context through, list subscriptions with the ARM SDK, validate the selected subscription ID, and resolve the tenant before provisioning credentials.
Azure service principal provisioning
cmd/configure_azure_sp.go, cmd/configure_azure_sp_test.go
Adds the Azure AD application and service principal orchestration, Graph and authorization-backed provisioning implementation, rollback handling, and unit tests for success, errors, rollback failure, and scope formatting.
GCP configure flow
cmd/configure_gcp.go
Adds ADC-backed Google API helpers, project listing, service account creation, IAM binding updates, key creation, and the context-aware interactive setup flow.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested labels: effort/xl

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: replacing Azure and GCP CLI shell-outs in configure flows with native SDK calls.
Docstring Coverage ✅ Passed Docstring coverage is 92.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/az-cli-to-sdk

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

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ci_cd_sanity_tests/pkg/sanity/azure/azure.go`:
- Line 263: The Subscription.State field is optional and is being dereferenced
unsafely in the azureSubscriptionInfo construction, which can panic when the SDK
omits it. Update the logic around the code that builds azureSubscriptionInfo in
the Azure sanity tests to guard sub.State before dereferencing it, following the
same nil-check pattern already used for other optional fields in this flow. If
the state is absent, leave the value empty or handle it consistently with the
existing optional-field handling.

In `@cmd/configure_azure.go`:
- Around line 70-72: The Azure Service Principal role name is inconsistent
between the help text and the wizard flow; update the documented role string in
configure_azure.go so it matches the role requested by the interactive command
in the Azure setup path (the configureAzure wizard and its related help/output),
and keep the same wording everywhere this role is referenced.
- Around line 261-266: The subscription picker is currently using
DefaultAzureCredential, which can resolve to a different principal than the
active az login session. Update listAzureSubscriptions to use Azure CLI auth
directly via azidentity.NewAzureCLICredential(nil), or adjust the credential
chain so AzureCLICredential is prioritized before other sources, keeping the
subscription list aligned with the rest of the wizard.

In `@cmd/configure_gcp.go`:
- Around line 533-536: The prompt handlers around read-and-confirm flows are
ignoring ReadString errors and treating empty input as approval, which can
trigger cloud mutations on EOF or read failure. Update the confirmation logic in
the createGCPServiceAccount path and the related role/key prompt handlers to
check the error returned by reader.ReadString before switching on the trimmed
choice, and abort with the corresponding error message instead of defaulting to
the run path when input cannot be read.
- Around line 288-410: The GCP SDK calls in listGCPProjects,
createGCPServiceAccount, grantGCPIAMRole, and createGCPServiceAccountKey still
use the inherited background context, so they can hang and block fallback
behavior. Update each helper to derive a short-lived timeout context before
calling newGCPAPIOption, cloudresourcemanager.NewService, iamv1.NewService, and
the subsequent Do()/Pages() calls. Keep the timeout scoped inside each helper
and ensure the derived context is used consistently for the client creation and
API request methods.
- Around line 357-386: The IAM policy update flow in the project policy helper
needs to preserve conditional bindings by using policy version 3. Update the
GetIamPolicy call in the policy modification logic to request version 3, then
ensure the returned policy has Version set to 3 before modifying bindings and
passing it to SetIamPolicy. Keep the fix localized to the
GetIamPolicy/SetIamPolicy sequence and the binding append logic in the project
IAM helper.
- Around line 607-628: The key-file flow in createGCPServiceAccountKey currently
returns keyFile even when no file was created or the fallback prompt was
skipped, which leaves getGCPCredentialsFilePath with a false path. Update the
switch handling for the run/skip/unknown cases so createGCPServiceAccountKey
only returns the actual keyFile path after a successful write, and returns an
empty string when the user skips, enters an unknown option, or declines/skips
promptAndRunGCPCommand. Make sure getGCPCredentialsFilePath can then detect the
empty result and continue prompting instead of assuming a file exists.
- Around line 407-421: The createGCPServiceAccountKey flow creates a remote IAM
key before the local file write succeeds, so a decode or os.WriteFile failure
can leave an orphaned active key. Update the logic around
createGCPServiceAccountKey to reserve the destination file first using exclusive
create semantics, and if any step after svc.Projects.ServiceAccounts.Keys.Create
fails, explicitly delete the newly created key before returning. Keep the
cleanup tied to the key creation result so fallback logic cannot mint duplicate
live keys.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 01d4076f-babd-499c-bcd3-2f549ec9ebb4

📥 Commits

Reviewing files that changed from the base of the PR and between 451a70f and 2653895.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • ci_cd_sanity_tests/pkg/sanity/azure/azure.go
  • ci_cd_sanity_tests/pkg/sanity/azure/azure_test.go
  • cmd/configure_azure.go
  • cmd/configure_gcp.go
  • go.mod

Comment thread ci_cd_sanity_tests/pkg/sanity/azure/azure.go Outdated
Comment thread cmd/configure_azure.go
Comment thread cmd/configure_azure.go Outdated
Comment thread cmd/configure_gcp.go
Comment thread cmd/configure_gcp.go Outdated
Comment thread cmd/configure_gcp.go Outdated
Comment thread cmd/configure_gcp.go Outdated
Comment thread cmd/configure_gcp.go Outdated
cristim added a commit that referenced this pull request Jun 24, 2026
Replace the remaining az/gcloud CLI fallbacks with native SDK calls that fail
loud, per owner decision on PR #1279.

Azure service principal creation (the create-for-rbac equivalent):
- New cmd/configure_azure_sp.go creates the AAD application registration
  (name "CUDly"), adds a password credential, creates the service principal,
  resolves the "Reservations Administrator" role definition by display name at
  subscription scope, and assigns it -- all via the Microsoft Graph SDK
  (applications + service principals + addPassword) and armauthorization/v2
  (RoleDefinitionsClient.NewListPager with a roleName filter,
  RoleAssignmentsClient.Create). DefaultAzureCredential reuses the az login
  session via its AzureCLICredential leg.
- The resulting appId (client ID), generated client secret, and tenant ID are
  printed in the same shape az ad sp create-for-rbac prints, so the operator
  can feed them into the credential collection step. The secret comes from the
  addPassword response (PasswordCredential.GetSecretText) and is only available
  at creation time.
- Tenant ID is resolved from the subscription via armsubscriptions.
- A narrow azureSPProvisioner interface wraps the four cloud operations so the
  orchestration is unit-testable; cmd/configure_azure_sp_test.go mocks it and
  asserts the app name = "CUDly", role = "Reservations Administrator", and
  scope = /subscriptions/<id>, plus per-step error propagation (no role
  resolve/assign after an earlier failure).

Remove silent CLI fallbacks -> fail loud:
- configure_azure.go subscription list: on SDK error, return an error telling
  the operator to run "az login" first (was: silently run az account list).
- configure_gcp.go project list, SA create, role grant, key create: on SDK
  error, return the error (was: print "falling back to gcloud" and run the CLI).
  ADC failures hint to run "gcloud auth application-default login".

Remaining cloud-CLI exec.Command calls (the only three left, each
//nolint:gosec with a precise reason -- interactive auth bootstrap or local
state, no SDK equivalent):
- az login (interactive browser OAuth)
- gcloud auth login (interactive browser OAuth)
- gcloud config set project (writes local ~/.config/gcloud state)

Removing the az ad sp create-for-rbac subprocess eliminates its gosec G204
finding; the three retained calls carry justified nolints.

go.mod: add github.com/microsoftgraph/msgraph-sdk-go v1.99.0 and
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/
armauthorization/v2 v2.2.0 as direct deps (root module). go mod tidy pulled
the kiota transitive deps and bumped azcore/azidentity/otel to satisfy
msgraph requirements.
@cristim

cristim commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmd/configure_gcp.go (1)

488-510: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate the selected project against the SDK results before writing local config.

listGCPProjects(ctx) only gates listing success, while validateGCPProjectID only checks format. A valid-looking typo can still be written by gcloud config set project on Line 510, leaving local gcloud state pointed at an inaccessible/nonexistent project before later SDK calls fail. Have listGCPProjects return the discovered project IDs and reject entries not in that set before Line 510.

Suggested shape
-	if err := listGCPProjects(ctx); err != nil {
+	projectIDs, err := listGCPProjects(ctx)
+	if err != nil {
 		return "", fmt.Errorf("failed to list GCP projects via SDK: %w\n"+
 			"Ensure Application Default Credentials are set: run 'gcloud auth application-default login' first", err)
 	}
@@
 	if err := validateGCPProjectID(projectID); err != nil {
 		return "", err
 	}
+	if _, ok := projectIDs[projectID]; !ok {
+		return "", fmt.Errorf("project ID %q was not found in the SDK project list", projectID)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/configure_gcp.go` around lines 488 - 510, The current flow in
configureGcpProject only checks that listGCPProjects(ctx) succeeds and that
validateGCPProjectID passes format checks, but it still lets an unknown project
ID be written by exec.Command("gcloud", "config", "set", "project", projectID).
Update listGCPProjects to return the discovered project IDs, then in
configureGcpProject verify the user-selected projectID exists in that returned
set before setting local gcloud config. Keep the existing validateGCPProjectID
check, but add a membership check against the SDK results and reject invalid
selections before the gcloud config write.
♻️ Duplicate comments (1)
cmd/configure_azure.go (1)

343-359: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use Azure CLI auth consistently for the interactive Azure wizard.

Step 1 explicitly establishes an az login session, but Step 2/3 switch to DefaultAzureCredential, which can resolve a different principal first. That can list subscriptions, resolve the tenant, or provision the service principal against the wrong account/subscription. Use azidentity.NewAzureCLICredential(nil) here (or a chain that puts Azure CLI first) and reuse that same credential for the rest of this wizard flow. This duplicates the earlier auth-chain finding and it still appears unresolved.

Also applies to: 376-395

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/configure_azure.go` around lines 343 - 359, The Azure wizard is still
using DefaultAzureCredential in the subscription/tenant/service-principal steps,
which can pick a different signed-in identity than the one established by Step
1. Update azureStepListSubscriptions and the related Step 3 flow to use
azidentity.NewAzureCLICredential(nil) or a shared credential chain that
prioritizes Azure CLI, and pass that same credential through the rest of the
interactive wizard so all operations target the same account/subscription.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/configure_azure_sp.go`:
- Around line 68-99: The createAzureServicePrincipal flow leaves partially
created Azure resources behind when later steps fail after CreateApplication
succeeds. Update createAzureServicePrincipal to add compensating cleanup or
recovery using the existing azureSPProvisioner methods and identifiers
(CreateApplication, AddPassword, CreateServicePrincipal,
ResolveRoleDefinitionID, AssignRole) so failures in password creation,
service-principal creation, or role assignment do not orphan the app/secret/SP.
Ensure the error paths either delete any already-created resources or detect and
reuse the existing application/service principal on retry rather than creating
duplicates.

---

Outside diff comments:
In `@cmd/configure_gcp.go`:
- Around line 488-510: The current flow in configureGcpProject only checks that
listGCPProjects(ctx) succeeds and that validateGCPProjectID passes format
checks, but it still lets an unknown project ID be written by
exec.Command("gcloud", "config", "set", "project", projectID). Update
listGCPProjects to return the discovered project IDs, then in
configureGcpProject verify the user-selected projectID exists in that returned
set before setting local gcloud config. Keep the existing validateGCPProjectID
check, but add a membership check against the SDK results and reject invalid
selections before the gcloud config write.

---

Duplicate comments:
In `@cmd/configure_azure.go`:
- Around line 343-359: The Azure wizard is still using DefaultAzureCredential in
the subscription/tenant/service-principal steps, which can pick a different
signed-in identity than the one established by Step 1. Update
azureStepListSubscriptions and the related Step 3 flow to use
azidentity.NewAzureCLICredential(nil) or a shared credential chain that
prioritizes Azure CLI, and pass that same credential through the rest of the
interactive wizard so all operations target the same account/subscription.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: be576c36-7e09-4fee-a364-bd5fb887ce7c

📥 Commits

Reviewing files that changed from the base of the PR and between 2653895 and 3e3df37.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • cmd/configure_azure.go
  • cmd/configure_azure_sp.go
  • cmd/configure_azure_sp_test.go
  • cmd/configure_gcp.go
  • go.mod

Comment thread cmd/configure_azure_sp.go
cristim added a commit that referenced this pull request Jun 24, 2026
Resolve the 7 Major CodeRabbit findings on PR #1279 at root cause.

1. azure sanity: guard sub.State before dereferencing it. State is an
   optional pointer; build azureSubscriptionInfo with the same nil-check
   pattern used for the other optional fields, so an omitted state cannot
   panic.

2. azure wizard: bind to the Azure CLI session explicitly. The wizard now
   builds credentials via a shared newAzureWizardCredential() backed by
   azidentity.NewAzureCLICredential, instead of DefaultAzureCredential whose
   chain prioritizes environment / workload / managed identity and could pick
   a different principal than the operator's "az login". The CI sanity test
   keeps DefaultAzureCredential (it must resolve the CI service-principal env
   vars). listAzureSubscriptions, resolveAzureTenantID and the SP provisioner
   all use the CLI credential.

3. gcp: bound every SDK helper with a 60s timeout. listGCPProjects,
   createGCPServiceAccount, grantGCPIAMRole and createGCPServiceAccountKey
   inherited context.Background() and could hang; each now derives a
   context.WithTimeout(ctx, gcpSDKCallTimeout).

4. gcp: preserve conditional IAM bindings. GetIamPolicy now requests
   RequestedPolicyVersion 3 and the policy is written back at Version 3, so a
   read-modify-write no longer silently drops condition bindings. Binding
   mutation is extracted into addMemberToPolicyBinding to keep complexity in
   check.

5. gcp: prevent orphaned service-account keys. createGCPServiceAccountKey
   reserves the destination file with O_EXCL before minting the remote key,
   and deletes the newly created remote key if decode/write fails, so a local
   failure cannot leave an active unused credential behind.

6. gcp: return an empty key path when no key was written. gcpStepCreateKey
   returns the key file path only after a successful write; on skip or an
   unknown choice it returns "" so getGCPCredentialsFilePath prompts for an
   existing credentials file instead of loading a missing one.

7. azure SP: roll back partial creation. createAzureServicePrincipal now
   deletes the just-created application (cascading to its password credential
   and derived service principal) if any later step -- add-password, SP
   create, role resolve, or role assign -- fails. A new DeleteApplication
   provisioner method backs this; if the compensating delete also fails the
   error names the orphaned app so the operator can remove it by hand. Unit
   tests assert rollback fires for each post-create failure, not on success,
   and that a failed rollback is surfaced.
@cristim

cristim commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

cristim added 4 commits July 4, 2026 00:46
… calls

Replace exec.Command shell-outs to cloud CLIs with native SDK calls where
the replacement is sound and does not require new heavyweight dependencies:

Azure (ci_cd_sanity_tests/):
- az account set/show -> armsubscriptions.Client.Get (programmatic CI path)
- az group list      -> armresources.ResourceGroupsClient.NewListPager
- az vm list         -> armcompute.VirtualMachinesClient.NewListAllPager

Azure (cmd/configure_azure.go):
- az account list    -> armsubscriptions.Client.NewListPager (with CLI fallback)

GCP (cmd/configure_gcp.go):
- gcloud projects list                 -> cloudresourcemanager/v1 Projects.List
- gcloud iam service-accounts create   -> iam/v1 Projects.ServiceAccounts.Create
- gcloud projects add-iam-policy-binding -> cloudresourcemanager/v1 SetIamPolicy
- gcloud iam service-accounts keys create -> iam/v1 SA Keys.Create (writes key
  data to file; key is base64-decoded from PrivateKeyData response field)

Calls left as CLI (documented in each function):
- az login / gcloud auth login: interactive browser OAuth; no SDK equivalent
  for an operator establishing an initial credential.
- gcloud config set project: writes to local gcloud config file; no API
  equivalent.
- az ad sp create-for-rbac: would require msgraph-sdk-go +
  armauthorization/v2, neither of which is in the module graph; the
  interactive wizard path does not justify adding two new heavy SDKs.

Auth model: DefaultAzureCredential (azure.go / configure_azure.go) picks up
the az login session via its AzureCLICredential leg. google.DefaultTokenSource
(configure_gcp.go) picks up gcloud application-default credentials; the wizard
now notes that operators must also run 'gcloud auth application-default login'
in addition to 'gcloud auth login' for the SDK steps to work.

go.mod: armcompute/v5 and armsubscriptions promoted from indirect to direct;
armresources v1.2.0 promoted from transitive to direct. No new SDK versions
or packages outside the existing module graph were added (google.golang.org/api
iam/v1 and cloudresourcemanager/v1 are sub-packages of the already-required
google.golang.org/api v0.274.0).

Removes the gosec G204 subprocess findings from ci_cd_sanity_tests/azure.go
(the only nolint-free exec.Command site in the CI path). The remaining
exec.Command calls in configure_azure.go and configure_gcp.go are in
interactive wizard paths where the CLI is intentionally required; those are
addressed separately.
Replace the remaining az/gcloud CLI fallbacks with native SDK calls that fail
loud, per owner decision on PR #1279.

Azure service principal creation (the create-for-rbac equivalent):
- New cmd/configure_azure_sp.go creates the AAD application registration
  (name "CUDly"), adds a password credential, creates the service principal,
  resolves the "Reservations Administrator" role definition by display name at
  subscription scope, and assigns it -- all via the Microsoft Graph SDK
  (applications + service principals + addPassword) and armauthorization/v2
  (RoleDefinitionsClient.NewListPager with a roleName filter,
  RoleAssignmentsClient.Create). DefaultAzureCredential reuses the az login
  session via its AzureCLICredential leg.
- The resulting appId (client ID), generated client secret, and tenant ID are
  printed in the same shape az ad sp create-for-rbac prints, so the operator
  can feed them into the credential collection step. The secret comes from the
  addPassword response (PasswordCredential.GetSecretText) and is only available
  at creation time.
- Tenant ID is resolved from the subscription via armsubscriptions.
- A narrow azureSPProvisioner interface wraps the four cloud operations so the
  orchestration is unit-testable; cmd/configure_azure_sp_test.go mocks it and
  asserts the app name = "CUDly", role = "Reservations Administrator", and
  scope = /subscriptions/<id>, plus per-step error propagation (no role
  resolve/assign after an earlier failure).

Remove silent CLI fallbacks -> fail loud:
- configure_azure.go subscription list: on SDK error, return an error telling
  the operator to run "az login" first (was: silently run az account list).
- configure_gcp.go project list, SA create, role grant, key create: on SDK
  error, return the error (was: print "falling back to gcloud" and run the CLI).
  ADC failures hint to run "gcloud auth application-default login".

Remaining cloud-CLI exec.Command calls (the only three left, each
//nolint:gosec with a precise reason -- interactive auth bootstrap or local
state, no SDK equivalent):
- az login (interactive browser OAuth)
- gcloud auth login (interactive browser OAuth)
- gcloud config set project (writes local ~/.config/gcloud state)

Removing the az ad sp create-for-rbac subprocess eliminates its gosec G204
finding; the three retained calls carry justified nolints.

go.mod: add github.com/microsoftgraph/msgraph-sdk-go v1.99.0 and
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/
armauthorization/v2 v2.2.0 as direct deps (root module). go mod tidy pulled
the kiota transitive deps and bumped azcore/azidentity/otel to satisfy
msgraph requirements.
Resolve the 7 Major CodeRabbit findings on PR #1279 at root cause.

1. azure sanity: guard sub.State before dereferencing it. State is an
   optional pointer; build azureSubscriptionInfo with the same nil-check
   pattern used for the other optional fields, so an omitted state cannot
   panic.

2. azure wizard: bind to the Azure CLI session explicitly. The wizard now
   builds credentials via a shared newAzureWizardCredential() backed by
   azidentity.NewAzureCLICredential, instead of DefaultAzureCredential whose
   chain prioritizes environment / workload / managed identity and could pick
   a different principal than the operator's "az login". The CI sanity test
   keeps DefaultAzureCredential (it must resolve the CI service-principal env
   vars). listAzureSubscriptions, resolveAzureTenantID and the SP provisioner
   all use the CLI credential.

3. gcp: bound every SDK helper with a 60s timeout. listGCPProjects,
   createGCPServiceAccount, grantGCPIAMRole and createGCPServiceAccountKey
   inherited context.Background() and could hang; each now derives a
   context.WithTimeout(ctx, gcpSDKCallTimeout).

4. gcp: preserve conditional IAM bindings. GetIamPolicy now requests
   RequestedPolicyVersion 3 and the policy is written back at Version 3, so a
   read-modify-write no longer silently drops condition bindings. Binding
   mutation is extracted into addMemberToPolicyBinding to keep complexity in
   check.

5. gcp: prevent orphaned service-account keys. createGCPServiceAccountKey
   reserves the destination file with O_EXCL before minting the remote key,
   and deletes the newly created remote key if decode/write fails, so a local
   failure cannot leave an active unused credential behind.

6. gcp: return an empty key path when no key was written. gcpStepCreateKey
   returns the key file path only after a successful write; on skip or an
   unknown choice it returns "" so getGCPCredentialsFilePath prompts for an
   existing credentials file instead of loading a missing one.

7. azure SP: roll back partial creation. createAzureServicePrincipal now
   deletes the just-created application (cascading to its password credential
   and derived service principal) if any later step -- add-password, SP
   create, role resolve, or role assign -- fails. A new DeleteApplication
   provisioner method backs this; if the compensating delete also fails the
   error names the orphaned app so the operator can remove it by hand. Unit
   tests assert rollback fires for each post-create failure, not on success,
   and that a failed rollback is surfaced.
The PR replaced CLI shell-outs with SDK calls and introduced new
interactive steps in the Azure SP and GCP SA wizards. Each step
discarded the error from reader.ReadString('\n') with the legacy
`choice, _ :=` shape inherited from the surrounding code, which
golangci-lint's errcheck (check-blank: true) flags as a new violation
and which conflicts with the project's "no silent fallbacks" rule
for fail-loud error handling.

Surface the read error from the four PR-introduced prompts so an
unexpected EOF / closed-stdin propagates an explicit error instead of
silently treating it as the default "run" choice:

  * cmd/configure_azure.go: azureStepCreateServicePrincipal
  * cmd/configure_gcp.go: gcpStepCreateServiceAccount,
    gcpStepGrantRole, gcpStepCreateKey

Also fix the new json.Marshal in
ci_cd_sanity_tests/pkg/sanity/azure/azure.go:encodeAccountJSON to check
the (impossible-for-this-struct) error and return nil so the caller
skips the expected-checks step rather than feeding the validator a
partially-encoded payload.

Pre-existing reader.ReadString errcheck patterns on the same files
(promptAndRun*Command, executeExplicit/GCPCommand, getGCPCredentialsFilePath,
gcpStepSelectProject, azureStepListSubscriptions) are present on main
and outside this PR's scope; tracked separately.

All cmd/ and ci_cd_sanity_tests/ tests pass (767 tests across 13
packages).
@cristim cristim force-pushed the refactor/az-cli-to-sdk branch from a526f1a to 42bc7ad Compare July 3, 2026 22:55
@cristim

cristim commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main (42bc7ad). Summary of conflict resolution below.

Conflict origin: main's PR #1343 (two commits: c7cd25af9 errcheck wave + 36d383813 reader-threading / readTrimmedLine) touched cmd/configure_azure.go and cmd/configure_gcp.go in the same regions that this PR rewrote.

Conflicts resolved across 4 commits:

Commit 1 (refactor: replace CLI shell-outs with SDK calls) - 1 conflict in configure_azure.go:

  • PR renamed createAzureServicePrincipal -> azureStepCreateServicePrincipal and extracted runCreateSPCommand; main had fixed errcheck violations on the old function. Resolution: PR's refactor wins (new name + extraction), but runCreateSPCommand's silent response, _ := reader.ReadString was upgraded to readTrimmedLine to apply main's errcheck standard.

Commit 2 (refactor: SDK-create Azure SP, no CLI fallback) - 3 conflicts in configure_azure.go, 1 in configure_gcp.go, 1 in go.mod:

  • azureStepCreateServicePrincipal body: PR's SDK call via createAzureServicePrincipal + printAzureSPResult wins; HEAD's stale runCreateSPCommand call is removed. Choice reading uses readTrimmedLine (EOF-tolerant) instead of PR's bare reader.ReadString.
  • executeExplicitCommand / executeGCPCommand signatures: PR removed the reader *bufio.Reader parameter (back to fresh bufio.NewReader(os.Stdin)); HEAD's reader-threading wins per the task requirement "no fresh bufio.NewReader(os.Stdin) mid-flow". Combined PR's context-comment with main's threading-rationale comment.
  • go.mod: kept jackc/pgx/v5 v5.9.2 (security bump from main) AND added microsoftgraph/msgraph-sdk-go v1.99.0 (from PR).

Commit 4 (fix: check stdin read errors in new SDK wizard steps) - 1 conflict in configure_azure.go:

  • PR's fix used bare reader.ReadString('\n') with error check; HEAD already had readTrimmedLine (which adds EOF tolerance on top of error checking). HEAD wins - strictly a superset.

Additional fix (amend on commit 4): During conflict resolution the azureStepListSubscriptions subscription-ID read was upgraded from _, _ := reader.ReadString to readTrimmedLine, but the error return was missing the empty-string first value (return fmt.Errorf(...) in a (string, error) function). Fixed and amended.

Standards applied to surviving shell-out code (promptAndRunExplicitCommand, executeExplicitCommand, promptAndRunGCPCommand, executeGCPCommand - the three retained CLI calls):

Gates:

  • go build ./...: pass
  • go vet ./cmd/...: pass (no findings)
  • golangci-lint run ./cmd/...: EXIT 0; findings in configure_azure.go and configure_gcp.go are all pre-existing on main (godot style, gocritic param-combine, gosec G117, noctx exec.Command, staticcheck De Morgan - none introduced by this PR)
  • go test ./cmd/... -count=1: 763 passed, 0 failed

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/configure_azure_sp.go`:
- Around line 195-246: The RoleAssignment creation path in
graphSPProvisioner.AssignRole should tolerate ARM propagation delay after
CreateServicePrincipal by retrying on PrincipalNotFound instead of failing
immediately. Add a bounded backoff/retry around g.roleAsgn.Create, detect the
PrincipalNotFound case from the returned error, and only return a fatal error
after the retry limit is exceeded; keep the existing CreateServicePrincipal,
ResolveRoleDefinitionID, and AssignRole flow unchanged otherwise.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e441242b-21c7-4255-984b-a26ade2ec38e

📥 Commits

Reviewing files that changed from the base of the PR and between 3e3df37 and 42bc7ad.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • ci_cd_sanity_tests/pkg/sanity/azure/azure.go
  • ci_cd_sanity_tests/pkg/sanity/azure/azure_test.go
  • cmd/configure_azure.go
  • cmd/configure_azure_sp.go
  • cmd/configure_azure_sp_test.go
  • cmd/configure_gcp.go
  • go.mod
🚧 Files skipped from review as they are similar to previous changes (5)
  • ci_cd_sanity_tests/pkg/sanity/azure/azure_test.go
  • ci_cd_sanity_tests/pkg/sanity/azure/azure.go
  • cmd/configure_azure.go
  • go.mod
  • cmd/configure_gcp.go

Comment thread cmd/configure_azure_sp.go
…rincipalNotFound

Azure AD replication is eventually consistent: a service principal created
moments ago may not yet be visible to the ARM role-assignment API in a different
region, returning PrincipalNotFound or ServicePrincipalNotFound. Without retry
the wizard fails immediately in this race window.

- Extract roleAssigner interface so the retry loop is unit-testable without
  hitting Azure.
- Add isPrincipalNotFoundErr helper covering the canonical error codes
  (PrincipalNotFound, ServicePrincipalNotFound) and the older message-body form
  ("does not exist in the directory").
- Retry g.roleAsgn.Create with bounded exponential back-off (5s initial,
  doubles to 30s cap, 3-minute total budget) on PrincipalNotFound; fail loud
  with a diagnostic message and the MS troubleshooting URL if the budget
  expires.
- Use ctx-aware select in the sleep to treat context cancellation as terminal
  (not accumulated as lastErr).
- Add graphSPProvisioner.retryInitial / retryBudget fields (set to constants in
  production, overridable to millisecond values in tests) so tests complete in
  <100ms.
- Add TestGraphSPProvisioner_AssignRole_* and TestIsPrincipalNotFoundErr
  covering: retry-then-succeed, budget-exhausted, non-retryable error,
  context-cancellation, and error-code detection.
@cristim

cristim commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Labels

priority/p2 Backlog-worthy triaged Item has been triaged type/chore Maintenance / non-user-visible

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant