|
| 1 | +package integrationtests |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "errors" |
| 6 | + "fmt" |
| 7 | + "net/http" |
| 8 | + "os" |
| 9 | + "strings" |
| 10 | + "testing" |
| 11 | + "time" |
| 12 | + |
| 13 | + "github.com/Azure/azure-sdk-for-go/sdk/azcore" |
| 14 | + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" |
| 15 | + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" |
| 16 | + log "github.com/sirupsen/logrus" |
| 17 | + |
| 18 | + "github.com/overmindtech/cli/go/discovery" |
| 19 | + "github.com/overmindtech/cli/go/sdp-go" |
| 20 | + "github.com/overmindtech/cli/go/sdpcache" |
| 21 | + "github.com/overmindtech/cli/sources" |
| 22 | + "github.com/overmindtech/cli/sources/azure/clients" |
| 23 | + "github.com/overmindtech/cli/sources/azure/manual" |
| 24 | + azureshared "github.com/overmindtech/cli/sources/azure/shared" |
| 25 | +) |
| 26 | + |
| 27 | +const ( |
| 28 | + // Azure only allows one Network Watcher per region per subscription. |
| 29 | + // We create a test Network Watcher in our integration test resource group. |
| 30 | + integrationTestNetworkWatcherTestName = "ovm-integ-test-nw" |
| 31 | +) |
| 32 | + |
| 33 | +func TestNetworkNetworkWatcherIntegration(t *testing.T) { |
| 34 | + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") |
| 35 | + if subscriptionID == "" { |
| 36 | + t.Skip("AZURE_SUBSCRIPTION_ID environment variable not set") |
| 37 | + } |
| 38 | + |
| 39 | + cred, err := azureshared.NewAzureCredential(t.Context()) |
| 40 | + if err != nil { |
| 41 | + t.Fatalf("Failed to create Azure credential: %v", err) |
| 42 | + } |
| 43 | + |
| 44 | + rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) |
| 45 | + if err != nil { |
| 46 | + t.Fatalf("Failed to create Resource Groups client: %v", err) |
| 47 | + } |
| 48 | + |
| 49 | + networkWatchersClient, err := armnetwork.NewWatchersClient(subscriptionID, cred, nil) |
| 50 | + if err != nil { |
| 51 | + t.Fatalf("Failed to create Network Watchers client: %v", err) |
| 52 | + } |
| 53 | + |
| 54 | + setupCompleted := false |
| 55 | + |
| 56 | + t.Run("Setup", func(t *testing.T) { |
| 57 | + ctx := t.Context() |
| 58 | + |
| 59 | + // Create resource group if it doesn't exist |
| 60 | + err := createResourceGroup(ctx, rgClient, integrationTestResourceGroup, integrationTestLocation) |
| 61 | + if err != nil { |
| 62 | + t.Fatalf("Failed to create resource group: %v", err) |
| 63 | + } |
| 64 | + |
| 65 | + // Create network watcher - Azure only allows one per region per subscription |
| 66 | + err = createNetworkWatcher(ctx, networkWatchersClient, integrationTestResourceGroup, integrationTestNetworkWatcherTestName, integrationTestLocation) |
| 67 | + if err != nil { |
| 68 | + // If we hit the limit, it means a Network Watcher already exists in another RG |
| 69 | + if strings.Contains(err.Error(), "NetworkWatcherCountLimitReached") { |
| 70 | + t.Skipf("Skipping: Azure allows only one Network Watcher per region. One already exists: %v", err) |
| 71 | + } |
| 72 | + t.Fatalf("Failed to create network watcher: %v", err) |
| 73 | + } |
| 74 | + |
| 75 | + // Wait for network watcher to be available |
| 76 | + err = waitForNetworkWatcherAvailable(ctx, networkWatchersClient, integrationTestResourceGroup, integrationTestNetworkWatcherTestName) |
| 77 | + if err != nil { |
| 78 | + t.Fatalf("Failed waiting for network watcher: %v", err) |
| 79 | + } |
| 80 | + |
| 81 | + setupCompleted = true |
| 82 | + }) |
| 83 | + |
| 84 | + t.Run("Run", func(t *testing.T) { |
| 85 | + if !setupCompleted { |
| 86 | + t.Skip("Skipping Run: Setup did not complete successfully") |
| 87 | + } |
| 88 | + |
| 89 | + t.Run("GetNetworkWatcher", func(t *testing.T) { |
| 90 | + ctx := t.Context() |
| 91 | + |
| 92 | + log.Printf("Retrieving network watcher %s in subscription %s, resource group %s", |
| 93 | + integrationTestNetworkWatcherTestName, subscriptionID, integrationTestResourceGroup) |
| 94 | + |
| 95 | + wrapper := manual.NewNetworkNetworkWatcher( |
| 96 | + clients.NewNetworkWatchersClient(networkWatchersClient), |
| 97 | + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, |
| 98 | + ) |
| 99 | + scope := wrapper.Scopes()[0] |
| 100 | + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) |
| 101 | + |
| 102 | + sdpItem, qErr := adapter.Get(ctx, scope, integrationTestNetworkWatcherTestName, true) |
| 103 | + if qErr != nil { |
| 104 | + t.Fatalf("Expected no error, got: %v", qErr) |
| 105 | + } |
| 106 | + |
| 107 | + if sdpItem == nil { |
| 108 | + t.Fatalf("Expected sdpItem to be non-nil") |
| 109 | + } |
| 110 | + |
| 111 | + uniqueAttrKey := sdpItem.GetUniqueAttribute() |
| 112 | + uniqueAttrValue, err := sdpItem.GetAttributes().Get(uniqueAttrKey) |
| 113 | + if err != nil { |
| 114 | + t.Fatalf("Failed to get unique attribute: %v", err) |
| 115 | + } |
| 116 | + |
| 117 | + if uniqueAttrValue != integrationTestNetworkWatcherTestName { |
| 118 | + t.Fatalf("Expected unique attribute value to be %s, got %s", integrationTestNetworkWatcherTestName, uniqueAttrValue) |
| 119 | + } |
| 120 | + |
| 121 | + log.Printf("Successfully retrieved network watcher %s", integrationTestNetworkWatcherTestName) |
| 122 | + }) |
| 123 | + |
| 124 | + t.Run("ListNetworkWatchers", func(t *testing.T) { |
| 125 | + ctx := t.Context() |
| 126 | + |
| 127 | + log.Printf("Listing network watchers in subscription %s, resource group %s", |
| 128 | + subscriptionID, integrationTestResourceGroup) |
| 129 | + |
| 130 | + wrapper := manual.NewNetworkNetworkWatcher( |
| 131 | + clients.NewNetworkWatchersClient(networkWatchersClient), |
| 132 | + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, |
| 133 | + ) |
| 134 | + scope := wrapper.Scopes()[0] |
| 135 | + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) |
| 136 | + |
| 137 | + listable, ok := adapter.(discovery.ListableAdapter) |
| 138 | + if !ok { |
| 139 | + t.Fatalf("Adapter does not support List operation") |
| 140 | + } |
| 141 | + |
| 142 | + sdpItems, err := listable.List(ctx, scope, true) |
| 143 | + if err != nil { |
| 144 | + t.Fatalf("Failed to list network watchers: %v", err) |
| 145 | + } |
| 146 | + |
| 147 | + if len(sdpItems) < 1 { |
| 148 | + t.Fatalf("Expected at least one network watcher, got %d", len(sdpItems)) |
| 149 | + } |
| 150 | + |
| 151 | + var found bool |
| 152 | + for _, item := range sdpItems { |
| 153 | + uniqueAttrKey := item.GetUniqueAttribute() |
| 154 | + if v, err := item.GetAttributes().Get(uniqueAttrKey); err == nil && v == integrationTestNetworkWatcherTestName { |
| 155 | + found = true |
| 156 | + break |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + if !found { |
| 161 | + t.Fatalf("Expected to find network watcher %s in the list", integrationTestNetworkWatcherTestName) |
| 162 | + } |
| 163 | + |
| 164 | + log.Printf("Found %d network watchers in resource group %s", len(sdpItems), integrationTestResourceGroup) |
| 165 | + }) |
| 166 | + |
| 167 | + t.Run("VerifyLinkedItems", func(t *testing.T) { |
| 168 | + ctx := t.Context() |
| 169 | + |
| 170 | + log.Printf("Verifying linked items for network watcher %s", integrationTestNetworkWatcherTestName) |
| 171 | + |
| 172 | + wrapper := manual.NewNetworkNetworkWatcher( |
| 173 | + clients.NewNetworkWatchersClient(networkWatchersClient), |
| 174 | + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, |
| 175 | + ) |
| 176 | + scope := wrapper.Scopes()[0] |
| 177 | + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) |
| 178 | + |
| 179 | + sdpItem, qErr := adapter.Get(ctx, scope, integrationTestNetworkWatcherTestName, true) |
| 180 | + if qErr != nil { |
| 181 | + t.Fatalf("Expected no error, got: %v", qErr) |
| 182 | + } |
| 183 | + |
| 184 | + linkedQueries := sdpItem.GetLinkedItemQueries() |
| 185 | + |
| 186 | + for _, query := range linkedQueries { |
| 187 | + q := query.GetQuery() |
| 188 | + if q == nil { |
| 189 | + t.Error("LinkedItemQuery has nil Query") |
| 190 | + continue |
| 191 | + } |
| 192 | + |
| 193 | + if q.GetType() == "" { |
| 194 | + t.Error("LinkedItemQuery has empty Type") |
| 195 | + } |
| 196 | + |
| 197 | + if q.GetMethod() != sdp.QueryMethod_GET && q.GetMethod() != sdp.QueryMethod_SEARCH { |
| 198 | + t.Errorf("LinkedItemQuery has invalid Method: %v", q.GetMethod()) |
| 199 | + } |
| 200 | + |
| 201 | + if q.GetQuery() == "" { |
| 202 | + t.Error("LinkedItemQuery has empty Query") |
| 203 | + } |
| 204 | + |
| 205 | + if q.GetScope() == "" { |
| 206 | + t.Error("LinkedItemQuery has empty Scope") |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + log.Printf("Verified %d linked item queries for network watcher %s", len(linkedQueries), integrationTestNetworkWatcherTestName) |
| 211 | + }) |
| 212 | + |
| 213 | + t.Run("VerifyItemAttributes", func(t *testing.T) { |
| 214 | + ctx := t.Context() |
| 215 | + |
| 216 | + log.Printf("Verifying item attributes for network watcher %s", integrationTestNetworkWatcherTestName) |
| 217 | + |
| 218 | + wrapper := manual.NewNetworkNetworkWatcher( |
| 219 | + clients.NewNetworkWatchersClient(networkWatchersClient), |
| 220 | + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, |
| 221 | + ) |
| 222 | + scope := wrapper.Scopes()[0] |
| 223 | + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) |
| 224 | + |
| 225 | + sdpItem, qErr := adapter.Get(ctx, scope, integrationTestNetworkWatcherTestName, true) |
| 226 | + if qErr != nil { |
| 227 | + t.Fatalf("Expected no error, got: %v", qErr) |
| 228 | + } |
| 229 | + |
| 230 | + if sdpItem.GetType() != azureshared.NetworkNetworkWatcher.String() { |
| 231 | + t.Errorf("Expected item type %s, got %s", azureshared.NetworkNetworkWatcher, sdpItem.GetType()) |
| 232 | + } |
| 233 | + |
| 234 | + expectedScope := fmt.Sprintf("%s.%s", subscriptionID, integrationTestResourceGroup) |
| 235 | + if sdpItem.GetScope() != expectedScope { |
| 236 | + t.Errorf("Expected scope %s, got %s", expectedScope, sdpItem.GetScope()) |
| 237 | + } |
| 238 | + |
| 239 | + if sdpItem.GetUniqueAttribute() != "name" { |
| 240 | + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) |
| 241 | + } |
| 242 | + |
| 243 | + if err := sdpItem.Validate(); err != nil { |
| 244 | + t.Fatalf("Item validation failed: %v", err) |
| 245 | + } |
| 246 | + |
| 247 | + log.Printf("Verified item attributes for network watcher %s", integrationTestNetworkWatcherTestName) |
| 248 | + }) |
| 249 | + }) |
| 250 | + |
| 251 | + t.Run("Teardown", func(t *testing.T) { |
| 252 | + ctx := t.Context() |
| 253 | + |
| 254 | + // Delete the network watcher we created |
| 255 | + err := deleteNetworkWatcher(ctx, networkWatchersClient, integrationTestResourceGroup, integrationTestNetworkWatcherTestName) |
| 256 | + if err != nil { |
| 257 | + t.Logf("Warning: Failed to delete network watcher %s: %v", integrationTestNetworkWatcherTestName, err) |
| 258 | + } |
| 259 | + }) |
| 260 | +} |
| 261 | + |
| 262 | +func createNetworkWatcher(ctx context.Context, client *armnetwork.WatchersClient, resourceGroup, name, location string) error { |
| 263 | + _, err := client.Get(ctx, resourceGroup, name, nil) |
| 264 | + if err == nil { |
| 265 | + log.Printf("Network watcher %s already exists, skipping creation", name) |
| 266 | + return nil |
| 267 | + } |
| 268 | + |
| 269 | + result, err := client.CreateOrUpdate(ctx, resourceGroup, name, armnetwork.Watcher{ |
| 270 | + Location: &location, |
| 271 | + Tags: map[string]*string{ |
| 272 | + "purpose": new("overmind-integration-tests"), |
| 273 | + }, |
| 274 | + }, nil) |
| 275 | + if err != nil { |
| 276 | + var respErr *azcore.ResponseError |
| 277 | + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusConflict { |
| 278 | + if _, getErr := client.Get(ctx, resourceGroup, name, nil); getErr == nil { |
| 279 | + log.Printf("Network watcher %s already exists (conflict), skipping", name) |
| 280 | + return nil |
| 281 | + } |
| 282 | + return fmt.Errorf("network watcher %s conflict but not retrievable: %w", name, err) |
| 283 | + } |
| 284 | + return fmt.Errorf("failed to create network watcher: %w", err) |
| 285 | + } |
| 286 | + |
| 287 | + log.Printf("Network watcher %s created: %v", name, result.Watcher.Name) |
| 288 | + return nil |
| 289 | +} |
| 290 | + |
| 291 | +func waitForNetworkWatcherAvailable(ctx context.Context, client *armnetwork.WatchersClient, resourceGroup, name string) error { |
| 292 | + maxAttempts := 20 |
| 293 | + pollInterval := 5 * time.Second |
| 294 | + maxNotFoundAttempts := 5 |
| 295 | + notFoundCount := 0 |
| 296 | + |
| 297 | + for attempt := 1; attempt <= maxAttempts; attempt++ { |
| 298 | + resp, err := client.Get(ctx, resourceGroup, name, nil) |
| 299 | + if err != nil { |
| 300 | + var respErr *azcore.ResponseError |
| 301 | + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { |
| 302 | + notFoundCount++ |
| 303 | + if notFoundCount >= maxNotFoundAttempts { |
| 304 | + return fmt.Errorf("network watcher %s not found after %d attempts", name, notFoundCount) |
| 305 | + } |
| 306 | + time.Sleep(pollInterval) |
| 307 | + continue |
| 308 | + } |
| 309 | + return fmt.Errorf("error checking network watcher: %w", err) |
| 310 | + } |
| 311 | + notFoundCount = 0 |
| 312 | + if resp.Properties != nil && resp.Properties.ProvisioningState != nil && *resp.Properties.ProvisioningState == armnetwork.ProvisioningStateSucceeded { |
| 313 | + log.Printf("Network watcher %s is available", name) |
| 314 | + return nil |
| 315 | + } |
| 316 | + time.Sleep(pollInterval) |
| 317 | + } |
| 318 | + return fmt.Errorf("timeout waiting for network watcher %s", name) |
| 319 | +} |
| 320 | + |
| 321 | +func deleteNetworkWatcher(ctx context.Context, client *armnetwork.WatchersClient, resourceGroup, name string) error { |
| 322 | + poller, err := client.BeginDelete(ctx, resourceGroup, name, nil) |
| 323 | + if err != nil { |
| 324 | + var respErr *azcore.ResponseError |
| 325 | + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { |
| 326 | + log.Printf("Network watcher %s already deleted", name) |
| 327 | + return nil |
| 328 | + } |
| 329 | + return fmt.Errorf("failed to begin delete network watcher: %w", err) |
| 330 | + } |
| 331 | + |
| 332 | + _, err = poller.PollUntilDone(ctx, nil) |
| 333 | + if err != nil { |
| 334 | + return fmt.Errorf("failed to delete network watcher: %w", err) |
| 335 | + } |
| 336 | + |
| 337 | + log.Printf("Network watcher %s deleted successfully", name) |
| 338 | + return nil |
| 339 | +} |
0 commit comments