Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/arm-bicep-e2e-v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
MPF_SPCLIENTID: ${{ secrets.MPF_SPCLIENTID }}
MPF_SPCLIENTSECRET: ${{ secrets.MPF_SPCLIENTSECRET }}
MPF_SPOBJECTID: ${{ secrets.MPF_SPOBJECTID }}
MPF_BICEPEXECPATH: "/usr/local/bin/bicep"
permissions:
contents: read
pull-requests: write
Expand Down Expand Up @@ -70,5 +71,14 @@ jobs:
tenant-id: ${{ secrets.MPF_TENANTID }}
subscription-id: ${{ secrets.MPF_SUBSCRIPTIONID }}

- name: 🧪 Run ARM Bicep E2E Tests
run: task teste2e:armbicep
- name: 🧪 Run ARM E2E Tests
run: task teste2e:arm

- name: 🧪 Run ARM CLI Tests
run: task testcli:arm

- name: 🧪 Run Bicep E2E Tests
run: task teste2e:bicep

- name: 🧪 Run Bicep CLI Tests
run: task testcli:bicep
4 changes: 2 additions & 2 deletions .github/workflows/az-mpf-e2e-arm-bicep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ name: ARM and Bicep E2E Tests
on:
workflow_dispatch:

schedule:
- cron: '0 0 * * *'
# schedule:
# - cron: '0 0 * * *'

concurrency:
group: shared_mpf_service_principal_workflow_group
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/az-mpf-e2e-terraform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ name: Terraform e2e tests
on:
workflow_dispatch:

schedule:
- cron: '0 2 * * *'
# schedule:
# - cron: '0 2 * * *'

concurrency:
group: shared_mpf_service_principal_workflow_group
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/terraform-e2e-v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,10 @@ jobs:
run: |
export MPF_TFPATH=$(which terraform)
echo "Terraform path: $MPF_TFPATH"
task teste2e:terraform
task teste2e:terraform

- name: 🧪 Run Terraform CLI Tests
run: |
export MPF_TFPATH=$(which terraform)
echo "Terraform path: $MPF_TFPATH"
task testcli:terraform
10 changes: 7 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ GOTEST = $(GOCMD) test
GOGET = $(GOCMD) get

# Name of the binary output
BINARY_NAME = az-mpf
BINARY_NAME = azmpf

# Main source file
# MAIN_FILE = main.go
Expand Down Expand Up @@ -74,9 +74,13 @@ build-all:
GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(OUTPUT_DIR)/$(BINARY_NAME)-linux-amd64 ./cmd
GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(OUTPUT_DIR)/$(BINARY_NAME)-windows-amd64.exe ./cmd

test-e2e: # arm and bicep tests
test-e2e-arm: # arm and bicep tests
@echo "Running end-to-end tests..."
$(GOTEST) ./e2eTests -v -run TestARM TestBicep
$(GOTEST) ./e2eTests -v -run TestARM

test-e2e-bicep: # bicep tests
@echo "Running end-to-end tests..."
$(GOTEST) ./e2eTests -v -run TestBicep

test-e2e-terraform: # terraform tests
@echo "Running end-to-end tests..."
Expand Down
53 changes: 50 additions & 3 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,16 +181,63 @@ tasks:
FORMAT: '{{if eq .GITHUB_ACTIONS "true"}}github-actions{{else}}pkgname-and-test-fails{{end}}'
LDFLAGS: "-s -w -X main.version=testUnit"

teste2e:armbicep:
desc: Run e2e tests for ARM and Bicep
testcli:arm:
desc: Run CLI tests for ARM
cmds:
- |
RESULT_LINE_COUNT=$(go run ./cmd arm --templateFilePath ./samples/templates/aks-private-subnet.json --parametersFilePath ./samples/templates/aks-private-subnet-parameters.json | wc -l)
echo "Result line count: $RESULT_LINE_COUNT"
if [ "$RESULT_LINE_COUNT" -ne 13 ]; then
echo "Expected 13 lines, got $RESULT_LINE_COUNT"
exit 1
fi
echo "Test passed: $RESULT_LINE_COUNT lines"

testcli:bicep:
desc: Run CLI tests for Bicep
cmds:
- |
RESULT_LINE_COUNT=$(go run ./cmd bicep --bicepFilePath ./samples/bicep/aks-private-subnet.bicep --parametersFilePath ./samples/bicep/aks-private-subnet-params.json | wc -l)
echo "Result line count: $RESULT_LINE_COUNT"
if [ "$RESULT_LINE_COUNT" -ne 13 ]; then
echo "Expected 13 lines, got $RESULT_LINE_COUNT"
exit 1
fi
echo "Test passed: $RESULT_LINE_COUNT lines"

testcli:terraform:
desc: Run CLI tests for Terraform
cmds:
- |
RESULT_LINE_COUNT=$(go run ./cmd terraform --workingDir ./samples/terraform/aci --varFilePath ./samples/terraform/aci/dev.vars.tfvars | wc -l)
echo "Result line count: $RESULT_LINE_COUNT"
if [ "$RESULT_LINE_COUNT" -ne 13 ]; then
echo "Expected 13 lines, got $RESULT_LINE_COUNT"
exit 1
fi
echo "Test passed: $RESULT_LINE_COUNT lines"

teste2e:arm:
desc: Run e2e tests for ARM
cmds:
- go clean -testcache
- 'gotestsum --format-hivis --format {{.FORMAT}} --junitfile "testresults.xml" -- ./e2eTests -run TestARM TestBicep -v -timeout 30m -ldflags="{{.LDFLAGS}}" -coverprofile="coverage.out" -covermode atomic'
- 'gotestsum --format-hivis --format {{.FORMAT}} --junitfile "testresults.xml" -- ./e2eTests -run TestARM -v -timeout 30m -ldflags="{{.LDFLAGS}}" -coverprofile="coverage.out" -covermode atomic'
# - task: test:getcover
vars:
FORMAT: '{{if eq .GITHUB_ACTIONS "true"}}github-actions{{else}}pkgname-and-test-fails{{end}}'
LDFLAGS: "-s -w -X main.version=testAcc"


teste2e:bicep:
desc: Run e2e tests for Bicep
cmds:
- go clean -testcache
- 'gotestsum --format-hivis --format {{.FORMAT}} --junitfile "testresults.xml" -- ./e2eTests -run TestBicep -v -timeout 30m -ldflags="{{.LDFLAGS}}" -coverprofile="coverage.out" -covermode atomic'
# - task: test:getcover
vars:
FORMAT: '{{if eq .GITHUB_ACTIONS "true"}}github-actions{{else}}pkgname-and-test-fails{{end}}'
LDFLAGS: "-s -w -X main.version=testAcc"

teste2e:terraform:
desc: Run e2e tests for Terraform
cmds:
Expand Down
24 changes: 18 additions & 6 deletions cmd/armCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ var flgDeploymentNamePfx string
var flgLocation string
var flgTemplateFilePath string
var flgParametersFilePath string
var flgSubscriptionScoped bool

// var flgFullDeployment bool

Expand Down Expand Up @@ -78,7 +79,9 @@ func NewARMCommand() *cobra.Command {
log.Errorf("Error marking flag required for ARM template parameters file path: %v\n", err)
}

armCmd.Flags().StringVarP(&flgLocation, "location", "", "eastus", "Location")
armCmd.Flags().StringVarP(&flgLocation, "location", "", "eastus2", "Location")

armCmd.Flags().BoolVarP(&flgSubscriptionScoped, "subscriptionScoped", "", false, "Is Deployment Subscription Scoped")

// armCmd.Flags().BoolVarP(&flgFullDeployment, "fullDeployment", "", false, "Full Deployment")

Expand All @@ -94,6 +97,8 @@ func getMPFARM(cmd *cobra.Command, args []string) {
log.Debugf("DeploymentNamePfx: %s\n", flgDeploymentNamePfx)
log.Infof("TemplateFilePath: %s\n", flgTemplateFilePath)
log.Infof("ParametersFilePath: %s\n", flgParametersFilePath)
log.Infof("Location: %s\n", flgLocation)
log.Infof("SubscriptionScoped: %t\n", flgSubscriptionScoped)

// validate if template and parameters files exists
if _, err := os.Stat(flgTemplateFilePath); os.IsNotExist(err) {
Expand Down Expand Up @@ -127,6 +132,8 @@ func getMPFARM(cmd *cobra.Command, args []string) {
TemplateFilePath: flgTemplateFilePath,
ParametersFilePath: flgParametersFilePath,
DeploymentName: deploymentName,
SubscriptionScoped: flgSubscriptionScoped,
Location: flgLocation,
}

var rgManager usecase.ResourceGroupManager
Expand All @@ -145,7 +152,11 @@ func getMPFARM(cmd *cobra.Command, args []string) {

mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, true, false, true)

displayOptions := getDislayOptions(flgShowDetailedOutput, flgJSONOutput, mpfConfig.ResourceGroup.ResourceGroupResourceID)
log.Infof("Show Detailed Output: %t\n", flgShowDetailedOutput)
log.Infof("JSON Output: %t\n", flgJSONOutput)
log.Infof("Subscription Resource ID: %s\n", mpfConfig.SubscriptionID)

displayOptions := getDislayOptions(flgShowDetailedOutput, flgJSONOutput, mpfConfig.SubscriptionID)

mpfResult, err := mpfService.GetMinimumPermissionsRequired()
if err != nil {
Expand All @@ -159,17 +170,18 @@ func getMPFARM(cmd *cobra.Command, args []string) {
displayResult(mpfResult, displayOptions)
}

func getDislayOptions(flgShowDetailedOutput bool, flgJSONOutput bool, rgResourceId string) presentation.DisplayOptions {
func getDislayOptions(flgShowDetailedOutput bool, flgJSONOutput bool, subscriptionID string) presentation.DisplayOptions {
return presentation.DisplayOptions{
ShowDetailedOutput: flgShowDetailedOutput,
JSONOutput: flgJSONOutput,
DefaultResourceGroupResourceID: rgResourceId,
ShowDetailedOutput: flgShowDetailedOutput,
JSONOutput: flgJSONOutput,
SubscriptionID: subscriptionID,
}
}

func displayResult(mpfResult domain.MPFResult, displayOptions presentation.DisplayOptions) {
resultDisplayer := presentation.NewMPFResultDisplayer(mpfResult, displayOptions)
err := resultDisplayer.DisplayResult(os.Stdout)

if err != nil {
log.Fatal(err)
}
Expand Down
20 changes: 17 additions & 3 deletions cmd/bicepCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ func NewBicepCommand() *cobra.Command {
log.Errorf("Error marking flag required for Bicep executable path: %v\n", err)
}

bicepCmd.Flags().StringVarP(&flgLocation, "location", "", "eastus", "Location")
bicepCmd.Flags().StringVarP(&flgLocation, "location", "", "eastus2", "Location")
bicepCmd.Flags().BoolVarP(&flgSubscriptionScoped, "subscriptionScoped", "", false, "Is Deployment Subscription Scoped")

// bicepCmd.Flags().BoolVarP(&flgFullDeployment, "fullDeployment", "", false, "Full Deployment")

Expand All @@ -98,6 +99,8 @@ func getMPFBicep(cmd *cobra.Command, args []string) {
log.Infof("BicepFilePath: %s\n", flgBicepFilePath)
log.Infof("ParametersFilePath: %s\n", flgParametersFilePath)
log.Infof("BicepExecPath: %s\n", flgBicepExecPath)
log.Infof("SubscriptionScoped: %t\n", flgSubscriptionScoped)
log.Infof("Location: %s\n", flgLocation)

// validate if template and parameters files exists
if _, err := os.Stat(flgBicepFilePath); os.IsNotExist(err) {
Expand Down Expand Up @@ -150,6 +153,8 @@ func getMPFBicep(cmd *cobra.Command, args []string) {
TemplateFilePath: armTemplatePath,
ParametersFilePath: flgParametersFilePath,
DeploymentName: deploymentName,
SubscriptionScoped: flgSubscriptionScoped,
Location: flgLocation,
}

var rgManager usecase.ResourceGroupManager
Expand All @@ -166,7 +171,11 @@ func getMPFBicep(cmd *cobra.Command, args []string) {
initialPermissionsToAdd = []string{"Microsoft.Resources/deployments/*", "Microsoft.Resources/subscriptions/operationresults/read"}
permissionsToAddToResult = []string{"Microsoft.Resources/deployments/read", "Microsoft.Resources/deployments/write"}

mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, true, false, true)
var autoCreateResourceGroup bool = true
if flgSubscriptionScoped {
autoCreateResourceGroup = false
}
mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, true, false, autoCreateResourceGroup)

mpfResult, err := mpfService.GetMinimumPermissionsRequired()
if err != nil {
Expand All @@ -180,7 +189,12 @@ func getMPFBicep(cmd *cobra.Command, args []string) {
log.Errorf("Error deleting Generated ARM template file: %v\n", err)
}

displayOptions := getDislayOptions(flgShowDetailedOutput, flgJSONOutput, mpfConfig.ResourceGroup.ResourceGroupResourceID)
// log.Infof("Displaying MPF Result: %v\n", mpfResult)
log.Infof("Show Detailed Output: %t\n", flgShowDetailedOutput)
log.Infof("JSON Output: %t\n", flgJSONOutput)
log.Infof("Subscription ID: %s\n", mpfConfig.SubscriptionID)

displayOptions := getDislayOptions(flgShowDetailedOutput, flgJSONOutput, mpfConfig.SubscriptionID)

if err != nil {
if len(mpfResult.RequiredPermissions) > 0 {
Expand Down
2 changes: 1 addition & 1 deletion cmd/terraformCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func getMPFTerraform(cmd *cobra.Command, args []string) {
deploymentAuthorizationCheckerCleaner = terraform.NewTerraformAuthorizationChecker(flgWorkingDir, flgTFPath, flgVarFilePath, flgImportExistingResourcesToState, flgTargetModule)
mpfService = usecase.NewMPFService(ctx, rgManager, spRoleAssignmentManager, deploymentAuthorizationCheckerCleaner, mpfConfig, initialPermissionsToAdd, permissionsToAddToResult, false, true, false)

displayOptions := getDislayOptions(flgShowDetailedOutput, flgJSONOutput, mpfConfig.ResourceGroup.ResourceGroupResourceID)
displayOptions := getDislayOptions(flgShowDetailedOutput, flgJSONOutput, mpfConfig.SubscriptionID)

mpfResult, err := mpfService.GetMinimumPermissionsRequired()
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions docs/commandline-flags-and-env-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ When used for Terraform, the verbose and debug flags show detailed logs from Ter
| resourceGroupNamePfx | MPF_RESOURCEGROUPNAMEPFX | Optional | Prefix for the resource group name. If not provided, default prefix is testdeployrg. For ARM deployments this temporary resource group is created |
| deploymentNamePfx | MPF_DEPLOYMENTNAMEPFX | Optional | Prefix for the deployment name. If not provided, default prefix is testDeploy. For ARM deployments this temporary deployment is created |
| location | MPF_LOCATION | Optional | Location for the resource group. If not provided, default location is eastus |
| subscriptionScoped | MPF_SUBSCRIPTIONSCOPED | Optional | This flag indicates whether the deployment is scoped to a subscription. If set, the deployment will target a subscription else it is resource group scoped deployment. |

### Bicep Flags

Expand All @@ -36,6 +37,7 @@ When used for Terraform, the verbose and debug flags show detailed logs from Ter
| resourceGroupNamePfx | MPF_RESOURCEGROUPNAMEPFX | Optional | Prefix for the resource group name. If not provided, default prefix is testdeployrg. For Bicep deployments this temporary resource group is created |
| deploymentNamePfx | MPF_DEPLOYMENTNAMEPFX | Optional | Prefix for the deployment name. If not provided, default prefix is testDeploy. For Bicep deployments this temporary deployment is created |
| location | MPF_LOCATION | Optional | Location for the resource group. If not provided, default location is eastus |
| subscriptionScoped | MPF_SUBSCRIPTIONSCOPED | Optional | This flag indicates whether the deployment is scoped to a subscription. If set, the deployment will target a subscription else it is resource group scoped deployment. |

## Terraform Flags

Expand Down
55 changes: 51 additions & 4 deletions docs/display-options.MD
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export MPF_SPCLIENTID=YOUR_SP_CLIENT_ID
export MPF_SPCLIENTSECRET=YOUR_SP_CLIENT_SECRET
export MPF_SPOBJECTID=YOUR_SP_OBJECT_ID

$ ./az-mpf arm --templateFilePath ./samples/templates/aks-private-subnet.json --parametersFilePath ./samples/templates/aks-private-subnet-parameters.json --showDetailedOutput
$ ./azmpf arm --templateFilePath ./samples/templates/aks-private-subnet.json --parametersFilePath ./samples/templates/aks-private-subnet-parameters.json --showDetailedOutput

------------------------------------------------------------------------------------------------------------------------------------------
Permissions Required:
Expand Down Expand Up @@ -63,7 +63,7 @@ export MPF_SPCLIENTID=YOUR_SP_CLIENT_ID
export MPF_SPCLIENTSECRET=YOUR_SP_CLIENT_SECRET
export MPF_SPOBJECTID=YOUR_SP_OBJECT_ID

$ ./az-mpf arm --templateFilePath ./samples/templates/aks-private-subnet.json --parametersFilePath ./samples/templates/aks-private-subnet-parameters.json --jsonOutput
$ ./azmpf arm --templateFilePath ./samples/templates/aks-private-subnet.json --parametersFilePath ./samples/templates/aks-private-subnet-parameters.json --jsonOutput

{
"/subscriptions/SSSSSSSS-SSSS-SSSS-SSSS-SSSSSSSSSSSS/resourceGroups/testdeployrg-1Gb2X44": [
Expand Down Expand Up @@ -103,7 +103,7 @@ export MPF_SPCLIENTID=YOUR_SP_CLIENT_ID
export MPF_SPCLIENTSECRET=YOUR_SP_CLIENT_SECRET
export MPF_SPOBJECTID=YOUR_SP_OBJECT_ID

$ ./az-mpf arm --templateFilePath ./samples/templates/aks-private-subnet.json --parametersFilePath ./samples/templates/aks-private-subnet-parameters.json --verbose
$ ./azmpf arm --templateFilePath ./samples/templates/aks-private-subnet.json --parametersFilePath ./samples/templates/aks-private-subnet-parameters.json --verbose

INFO[0000] Executing MPF for ARM
INFO[0000] TemplateFilePath: ./samples/templates/aks-private-subnet.json
Expand Down Expand Up @@ -167,7 +167,7 @@ export MPF_SPCLIENTID=YOUR_SP_CLIENT_ID
export MPF_SPCLIENTSECRET=YOUR_SP_CLIENT_SECRET
export MPF_SPOBJECTID=YOUR_SP_OBJECT_ID

$ ./az-mpf arm --templateFilePath ./samples/templates/multi-resource-template.json --parametersFilePath ./samples/templates/multi-resource-parameters.json --showDetailedOutput
$ ./azmpf arm --templateFilePath ./samples/templates/multi-resource-template.json --parametersFilePath ./samples/templates/multi-resource-parameters.json --showDetailedOutput

------------------------------------------------------------------------------------------------------------------------------------------
Permissions Required:
Expand Down Expand Up @@ -470,3 +470,50 @@ Microsoft.Compute/virtualMachines/extensions/read
Microsoft.Compute/virtualMachines/extensions/write
--------------
```

### Subscription scoped ARM deployment

The --subscriptionScoped flag indicates whether the deployment is scoped to a subscription. If set, the deployment will target a subscription else it is resource group scoped deployment. The following is a sample of subscription scoped ARM deployment:

```shell
export MPF_SUBSCRIPTIONID=YOUR_SUBSCRIPTION_ID
export MPF_TENANTID=YOUR_TENANT_ID
export MPF_SPCLIENTID=YOUR_SP_CLIENT_ID
export MPF_SPCLIENTSECRET=YOUR_SP_CLIENT_SECRET
export MPF_SPOBJECTID=YOUR_SP_OBJECT_ID
export MPF_BICEPEXECPATH="/opt/homebrew/bin/bicep" # Path to the Bicep executable

$ ./azmpf arm --templateFilePath ./samples/templates/subscription-scope-create-rg.json --parametersFilePath ./samples/templates/subscription-scope-create-rg-params.json --subscriptionScoped --location eastus2
------------------------------------------------------------------------------------------------------------------------------------------
Permissions Required:
------------------------------------------------------------------------------------------------------------------------------------------
Microsoft.Resources/deployments/read
Microsoft.Resources/deployments/write
Microsoft.Resources/subscriptions/resourceGroups/read
Microsoft.Resources/subscriptions/resourceGroups/write
------------------------------------------------------------------------------------------------------------------------------------------
```

### Subscription scoped Bicep deployment

The --subscriptionScoped flag indicates whether the deployment is scoped to a subscription. If set, the deployment will target a subscription else it is resource group scoped deployment. The following is a sample of subscription scoped Bicep deployment:

```shell
export MPF_SUBSCRIPTIONID=YOUR_SUBSCRIPTION_ID
export MPF_TENANTID=YOUR_TENANT_ID
export MPF_SPCLIENTID=YOUR_SP_CLIENT_ID
export MPF_SPCLIENTSECRET=YOUR_SP_CLIENT_SECRET
export MPF_SPOBJECTID=YOUR_SP_OBJECT_ID
export MPF_BICEPEXECPATH="/opt/homebrew/bin/bicep" # Path to the Bicep executable

$ ./azmpf bicep --bicepFilePath ./samples/bicep/subscription-scope-create-rg.bicep --parametersFilePath ./samples/bicep/subscription-scope-create-rg-params.json --subscriptionScoped --location eastus2
------------------------------------------------------------------------------------------------------------------------------------------
Permissions Required:
------------------------------------------------------------------------------------------------------------------------------------------
Microsoft.Resources/deployments/read
Microsoft.Resources/deployments/write
Microsoft.Resources/subscriptions/resourceGroups/read
Microsoft.Resources/subscriptions/resourceGroups/write
------------------------------------------------------------------------------------------------------------------------------------------
```

Loading