Skip to content

Commit b28893f

Browse files
committed
FIX: master key handeling
1 parent 297948b commit b28893f

File tree

11 files changed

+430
-108
lines changed

11 files changed

+430
-108
lines changed
File renamed without changes.

.github/workflows/deploy-infra.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121

2222
defaults:
2323
run:
24-
working-directory: ./test/tf
24+
working-directory: ./.github/tf
2525

2626
steps:
2727
- name: Checkout code

.github/workflows/orchestrator.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ jobs:
4545
uses: ./.github/workflows/test-go-app.yml
4646
with:
4747
environment: ${{ github.ref_name }}
48-
terraform_version: "1.14.4"
4948
secrets: inherit
5049

5150
release:

.github/workflows/test-go-app.yml

Lines changed: 19 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ on:
66
environment:
77
required: true
88
type: string
9-
terraform_version:
10-
required: true
11-
type: string
129

1310
permissions:
1411
id-token: write
@@ -30,65 +27,23 @@ jobs:
3027
with:
3128
go-version-file: go.mod
3229

33-
- name: Set up Terraform
34-
uses: hashicorp/setup-terraform@v2
35-
with:
36-
terraform_version: ${{ inputs.terraform_version }}
37-
terraform_wrapper: false
38-
3930
- name: Azure Login (OIDC)
4031
uses: azure/login@v2
4132
with:
4233
client-id: ${{ secrets.AZURE_CLIENT_ID }}
4334
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
4435
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
4536

46-
- name: Configure Terraform for Azure
47-
run: |
48-
echo "ARM_CLIENT_ID=${{ secrets.AZURE_CLIENT_ID }}" >> $GITHUB_ENV
49-
echo "ARM_TENANT_ID=${{ secrets.AZURE_TENANT_ID }}" >> $GITHUB_ENV
50-
echo "ARM_SUBSCRIPTION_ID=${{ secrets.AZURE_SUBSCRIPTION_ID }}" >> $GITHUB_ENV
51-
echo "ARM_USE_OIDC=true" >> $GITHUB_ENV
52-
5337
- name: Build Go application
5438
run: go build -o kura .
5539

56-
- name: Terraform Init
57-
working-directory: ./test/tf
58-
run: |
59-
terraform init \
60-
-backend-config="resource_group_name=apim-kura" \
61-
-backend-config="storage_account_name=apimkura" \
62-
-backend-config="container_name=devops" \
63-
-backend-config="key=${{ inputs.environment }}.tfstate"
64-
6540
- name: Fetch subscription keys (before)
6641
run: |
67-
set -euo pipefail
68-
AZURE_SUB=${{ secrets.AZURE_SUBSCRIPTION_ID }}
69-
API_VERSION="2022-08-01"
70-
RG="${{ env.RESOURCE_GROUP }}"
71-
APIM="${{ env.APIM_NAME }}"
72-
BASE_URL="https://management.azure.com/subscriptions/${AZURE_SUB}/resourceGroups/${RG}/providers/Microsoft.ApiManagement/service/${APIM}"
73-
74-
SUBS=$(az rest --method get --url "${BASE_URL}/subscriptions?api-version=${API_VERSION}")
75-
76-
RESULT="[]"
77-
for SID in $(echo "$SUBS" | jq -r '.value[] | select(.properties.displayName | startswith("Built-in") | not) | .name'); do
78-
SECRETS=$(az rest --method post --url "${BASE_URL}/subscriptions/${SID}/listSecrets?api-version=${API_VERSION}")
79-
DISPLAY_NAME=$(echo "$SUBS" | jq -r --arg sid "$SID" '.value[] | select(.name == $sid) | .properties.displayName')
80-
PRIMARY=$(echo "$SECRETS" | jq -r '.primaryKey')
81-
SECONDARY=$(echo "$SECRETS" | jq -r '.secondaryKey')
82-
83-
RESULT=$(echo "$RESULT" | jq \
84-
--arg dn "$DISPLAY_NAME" \
85-
--arg pk "$PRIMARY" \
86-
--arg sk "$SECONDARY" \
87-
'. + [{"displayName": $dn, "primaryKey": $pk, "secondaryKey": $sk}]')
88-
done
89-
90-
echo "$RESULT" | jq 'sort_by(.displayName)' > before.json
91-
echo "Saved $(echo "$RESULT" | jq length) subscription keys to before.json"
42+
./kura backup \
43+
--resource-group "${{ env.RESOURCE_GROUP }}" \
44+
--apim-name "${{ env.APIM_NAME }}" \
45+
--subscription "${{ secrets.AZURE_SUBSCRIPTION_ID }}" \
46+
--output before.json
9247
9348
- name: Run kura backup
9449
run: |
@@ -98,14 +53,11 @@ jobs:
9853
--subscription "${{ secrets.AZURE_SUBSCRIPTION_ID }}"
9954
10055
- name: Delete all subscription resources
101-
working-directory: ./test/tf
10256
run: |
103-
terraform destroy \
104-
-target='azurerm_api_management_subscription.global_subscription' \
105-
-target='azurerm_api_management_subscription.product_subscription' \
106-
-target='azurerm_api_management_subscription.api_subscription' \
107-
-var="environment=${{ inputs.environment }}" \
108-
-auto-approve
57+
./kura delete \
58+
--resource-group "${{ env.RESOURCE_GROUP }}" \
59+
--apim-name "${{ env.APIM_NAME }}" \
60+
--subscription "${{ secrets.AZURE_SUBSCRIPTION_ID }}"
10961
11062
- name: Run kura restore
11163
run: |
@@ -117,46 +69,18 @@ jobs:
11769
11870
- name: Fetch subscription keys (after)
11971
run: |
120-
set -euo pipefail
121-
AZURE_SUB=$(az account show --query id -o tsv)
122-
API_VERSION="2022-08-01"
123-
RG="${{ env.RESOURCE_GROUP }}"
124-
APIM="${{ env.APIM_NAME }}"
125-
BASE_URL="https://management.azure.com/subscriptions/${AZURE_SUB}/resourceGroups/${RG}/providers/Microsoft.ApiManagement/service/${APIM}"
126-
127-
SUBS=$(az rest --method get --url "${BASE_URL}/subscriptions?api-version=${API_VERSION}")
128-
129-
RESULT="[]"
130-
for SID in $(echo "$SUBS" | jq -r '.value[] | select(.properties.displayName | startswith("Built-in") | not) | .name'); do
131-
SECRETS=$(az rest --method post --url "${BASE_URL}/subscriptions/${SID}/listSecrets?api-version=${API_VERSION}")
132-
DISPLAY_NAME=$(echo "$SUBS" | jq -r --arg sid "$SID" '.value[] | select(.name == $sid) | .properties.displayName')
133-
PRIMARY=$(echo "$SECRETS" | jq -r '.primaryKey')
134-
SECONDARY=$(echo "$SECRETS" | jq -r '.secondaryKey')
135-
136-
RESULT=$(echo "$RESULT" | jq \
137-
--arg dn "$DISPLAY_NAME" \
138-
--arg pk "$PRIMARY" \
139-
--arg sk "$SECONDARY" \
140-
'. + [{"displayName": $dn, "primaryKey": $pk, "secondaryKey": $sk}]')
141-
done
142-
143-
echo "$RESULT" | jq 'sort_by(.displayName)' > after.json
144-
echo "Saved $(echo "$RESULT" | jq length) subscription keys to after.json"
72+
./kura backup \
73+
--resource-group "${{ env.RESOURCE_GROUP }}" \
74+
--apim-name "${{ env.APIM_NAME }}" \
75+
--subscription "${{ secrets.AZURE_SUBSCRIPTION_ID }}" \
76+
--output after.json
14577
14678
- name: Compare subscription keys
14779
run: |
148-
echo "=== Before (subscription count): $(jq length before.json) ==="
149-
echo "=== After (subscription count): $(jq length after.json) ==="
150-
151-
if diff <(jq -S . before.json) <(jq -S . after.json); then
152-
echo "✓ All subscription keys match! Backup and restore successful."
153-
else
154-
echo ""
155-
echo "✗ Subscription keys do not match after restore!"
156-
echo ""
157-
echo "--- Before ---"
158-
jq . before.json
159-
echo "--- After ---"
160-
jq . after.json
80+
# test if before.json and after.json are not empty
81+
if [ ! -s before.json ] || [ ! -s after.json ]; then
82+
echo "One of the backup files is empty!"
16183
exit 1
16284
fi
85+
86+
./kura compare before.json after.json

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Azure API Management subscription keys are critical credentials that grant acces
1313
- [backup](#backup)
1414
- [restore](#restore)
1515
- [list](#list)
16+
- [compare](#compare)
17+
- [delete](#delete)
1618
- [clean](#clean)
1719
- [Backup Storage Layout](#backup-storage-layout)
1820
- [Typical Workflow](#typical-workflow)
@@ -25,6 +27,20 @@ Azure API Management subscription keys are critical credentials that grant acces
2527

2628
## Installation
2729

30+
### Pre-built binaries
31+
32+
Download the latest release from the [GitHub Releases](https://github.com/f-marschall/apim-kura/releases) page. Binaries are available for Linux, macOS, and Windows across multiple architectures.
33+
34+
For example, on Linux (amd64):
35+
36+
```bash
37+
curl -Lo kura https://github.com/f-marschall/apim-kura/releases/latest/download/kura-linux-amd64
38+
chmod +x kura
39+
sudo mv kura /usr/local/bin/
40+
```
41+
42+
### Build from source
43+
2844
```bash
2945
git clone https://github.com/f-marschall/apim-kura.git
3046
cd apim-kura
@@ -100,6 +116,29 @@ When `--product-id` is provided, the output is filtered to subscriptions scoped
100116
| `--product-id` | `-p` | No | Filter output to a single product |
101117
| `--subscription` | `-s` | No | Azure subscription ID (defaults to current CLI context) |
102118

119+
### compare
120+
121+
```
122+
kura compare <file1> <file2>
123+
```
124+
125+
The compare command reads two backup JSON files and displays the differences between them. Use this to audit changes, verify backup consistency, or compare subscription keys across different snapshots.
126+
127+
### delete
128+
129+
```
130+
kura delete --resource-group <rg> --apim-name <apim> --subscription-id <id> [--subscription <sub-id>]
131+
```
132+
133+
The delete command removes a subscription from an APIM instance. Specify the subscription ID (GUID) to delete.
134+
135+
| Flag | Short | Required | Description |
136+
|------|-------|----------|----------|
137+
| `--resource-group` | `-g` | Yes | Azure resource group containing the APIM instance |
138+
| `--apim-name` | `-a` | Yes | Name of the APIM instance |
139+
| `--subscription-id` | `-i` | Yes | The subscription ID (GUID) to delete |
140+
| `--subscription` | `-s` | No | Azure subscription ID (defaults to current CLI context) |
141+
103142
### clean
104143

105144
```

cmd/backup.go

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ var backupCmd = &cobra.Command{
1616
Use: "backup",
1717
Short: "Backup subscription keys from Azure API Management",
1818
Long: `Backup retrieves subscription keys from an Azure API Management instance
19-
and saves them to a local backup directory.
19+
and saves them to a local backup directory or file.
2020
21-
The backup is stored under: backup/<resource-group>/<apim-name>[/<product-id>]
21+
By default, backups are stored under: backup/<resource-group>/<apim-name>[/<product-id>]
22+
Use --output to save to a custom file path instead.
2223
2324
Example:
2425
kura backup --resource-group mygroup --apim-name myapim
25-
kura backup --resource-group mygroup --apim-name myapim --product-id myproduct`,
26+
kura backup --resource-group mygroup --apim-name myapim --product-id myproduct
27+
kura backup -g mygroup -a myapim --output ./my-backup.json`,
2628
RunE: runBackup,
2729
}
2830

@@ -31,6 +33,7 @@ var (
3133
backupAPIMName string
3234
backupSubscription string
3335
backupProductID string
36+
backupOutput string
3437
)
3538

3639
func init() {
@@ -41,6 +44,7 @@ func init() {
4144
backupCmd.Flags().StringVarP(&backupAPIMName, "apim-name", "a", "", "Azure API Management instance name (required)")
4245
backupCmd.Flags().StringVarP(&backupSubscription, "subscription", "s", "", "Azure subscription ID")
4346
backupCmd.Flags().StringVarP(&backupProductID, "product-id", "p", "", "Azure APIM product ID (optional, scopes backup to a product)")
47+
backupCmd.Flags().StringVarP(&backupOutput, "output", "o", "", "Output file path (if not specified, defaults to backup folder structure)")
4448

4549
// Mark required flags
4650
backupCmd.MarkFlagRequired("resource-group")
@@ -58,12 +62,20 @@ func runBackup(cmd *cobra.Command, args []string) error {
5862
fmt.Printf("Product ID: %s\n", backupProductID)
5963
}
6064

61-
// Create backup directory structure
62-
backupDir, err := backup.EnsureBackupDir(backupResourceGroup, backupAPIMName, backupProductID)
63-
if err != nil {
64-
return fmt.Errorf("failed to create backup directory: %w", err)
65+
// Determine output file path
66+
var filePath string
67+
if backupOutput != "" {
68+
filePath = backupOutput
69+
fmt.Printf("Output file: %s\n", filePath)
70+
} else {
71+
// Create backup directory structure
72+
backupDir, err := backup.EnsureBackupDir(backupResourceGroup, backupAPIMName, backupProductID)
73+
if err != nil {
74+
return fmt.Errorf("failed to create backup directory: %w", err)
75+
}
76+
filePath = filepath.Join(backupDir, "subscriptions.json")
77+
fmt.Printf("Backup directory: %s\n", backupDir)
6578
}
66-
fmt.Printf("Backup directory: %s\n", backupDir)
6779

6880
// Authenticate with Azure CLI
6981
ctx := context.Background()
@@ -73,8 +85,6 @@ func runBackup(cmd *cobra.Command, args []string) error {
7385
if err != nil {
7486
return fmt.Errorf("authentication failed: %w", err)
7587
}
76-
fmt.Println("Successfully authenticated with Azure CLI")
77-
7888
fmt.Println("\nFetching subscriptions...")
7989
subs, err := client.ListSubscriptions(ctx, backupProductID)
8090

@@ -85,7 +95,16 @@ func runBackup(cmd *cobra.Command, args []string) error {
8595
return fmt.Errorf("failed to marshal subscriptions to JSON: %w", err)
8696
}
8797

88-
filePath := filepath.Join(backupDir, "subscriptions.json")
98+
// Ensure parent directories exist if using custom output path
99+
if backupOutput != "" {
100+
dir := filepath.Dir(filePath)
101+
if dir != "." && dir != "" {
102+
if err := os.MkdirAll(dir, 0755); err != nil {
103+
return fmt.Errorf("failed to create output directory: %w", err)
104+
}
105+
}
106+
}
107+
89108
if err := os.WriteFile(filePath, prettyJSON, 0644); err != nil {
90109
return fmt.Errorf("failed to write backup file: %w", err)
91110
}

0 commit comments

Comments
 (0)