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
10 changes: 10 additions & 0 deletions server/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,12 @@ func New(d Drivers) http.Handler {
if d.LB != nil {
lbHandler = lbsrv.New(d.LB)

if vnetHandler != nil {
if az, ok := d.LB.(lbdriver.AzureLoadBalancers); ok {
vnetHandler.SetLBResolver(az)
}
}

// Project a backend address pool's read-only backendIPConfigurations from
// the NIC side of the association, so NIC↔LB pool membership reflects on
// both sides. Only wired when the networking driver exposes NICs.
Expand Down Expand Up @@ -510,6 +516,10 @@ func New(d Drivers) http.Handler {
// cascade. Registered further below.
if d.PrivateDNS != nil {
privateDNS = privatednssrv.New(d.PrivateDNS)

if vnetHandler != nil {
privateDNS.SetVNetResolver(vnetHandler.VNetExists)
}
}

if d.BlobStorage != nil {
Expand Down
7 changes: 7 additions & 0 deletions server/azure/privatedns/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ import (
// Private DNS driver.
type Handler struct {
pdns pddriver.PrivateDNS
// vnetExists validates a link's virtualNetwork reference; nil skips it.
vnetExists func(ctx context.Context, vnetID string) bool
}

// SetVNetResolver wires the virtual network lookup used to validate links.
func (h *Handler) SetVNetResolver(f func(ctx context.Context, vnetID string) bool) {
h.vnetExists = f
}

// New returns an Azure Private DNS handler backed by d.
Expand Down
6 changes: 6 additions & 0 deletions server/azure/privatedns/link_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ func (h *Handler) createOrUpdateLink(w http.ResponseWriter, r *http.Request, rp

link := buildLink(&body)

if h.vnetExists != nil && link.VirtualNetworkID != "" && !h.vnetExists(r.Context(), link.VirtualNetworkID) {
azurearm.WriteError(w, http.StatusBadRequest, "InvalidResourceReference",
"Resource "+link.VirtualNetworkID+" referenced by resource was not found.")
return
}

stored, created, err := h.pdns.CreateOrUpdateVirtualNetworkLink(
r.Context(), rp.ResourceGroup, rp.ResourceName, rp.SubResourceName, link)
if err != nil {
Expand Down
44 changes: 44 additions & 0 deletions server/azure/privatedns/link_vnet_reference_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package privatedns_test

import (
"net/http"
"testing"
)

func TestLinkToMissingVNetRejected(t *testing.T) {
ts := newServer(t)

if code, _ := doJSON(t, ts, http.MethodPut, zonePath(zoneName),
map[string]any{"location": "global"}); code/100 != 2 {
t.Fatalf("zone create: %d", code)
}

vnetBase := "/subscriptions/" + testSub + "/resourceGroups/" + testRG +
"/providers/Microsoft.Network/virtualNetworks/"
linkPath := zonePath(zoneName) + "/virtualNetworkLinks/link-x"
link := func(vnet string) map[string]any {
return map[string]any{
"location": "global",
"properties": map[string]any{
"virtualNetwork": map[string]any{"id": vnetBase + vnet},
"registrationEnabled": false,
},
}
}

code, body := doJSON(t, ts, http.MethodPut, linkPath, link("ghost"))
if code != http.StatusBadRequest {
t.Fatalf("link to missing vnet: %d %v, want 400", code, body)
}

if code, _ = doJSON(t, ts, http.MethodPut, vnetBase+"real", map[string]any{
"location": "eastus",
"properties": map[string]any{"addressSpace": map[string]any{"addressPrefixes": []string{"10.0.0.0/16"}}},
}); code/100 != 2 {
t.Fatalf("vnet create: %d", code)
}

if code, body = doJSON(t, ts, http.MethodPut, linkPath, link("real")); code != http.StatusCreated {
t.Fatalf("link to existing vnet: %d %v, want 201", code, body)
}
}
7 changes: 7 additions & 0 deletions server/azure/privatedns/roundtrip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ func TestLinkRegistrationFalseRoundTrips(t *testing.T) {
"/providers/Microsoft.Network/virtualNetworks/vnet-1"
linkPath := zonePath(zoneName) + "/virtualNetworkLinks/link-1"

if code, _ := doJSON(t, ts, http.MethodPut, vnetID, map[string]any{
"location": "eastus",
"properties": map[string]any{"addressSpace": map[string]any{"addressPrefixes": []string{"10.0.0.0/16"}}},
}); code/100 != 2 {
t.Fatalf("vnet create: %d", code)
}

status, created := doJSON(t, ts, http.MethodPut, linkPath, map[string]any{
"location": "Global",
"properties": map[string]any{
Expand Down
2 changes: 2 additions & 0 deletions server/azure/vnet/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
cerrors "github.com/stackshy/cloudemu/v2/errors"
"github.com/stackshy/cloudemu/v2/server/azure/resourcegroups"
"github.com/stackshy/cloudemu/v2/server/wire/azurearm"
lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)

Expand Down Expand Up @@ -124,6 +125,7 @@ type Handler struct {
// not atomic). It guards only that get-modify-put window.
patchMu sync.Mutex
net netdriver.Networking
lbs lbdriver.AzureLoadBalancers
}

// New returns a network handler.
Expand Down
70 changes: 70 additions & 0 deletions server/azure/vnet/network_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package vnet

import (
"context"
"errors"
"net/http"
"strings"

cerrors "github.com/stackshy/cloudemu/v2/errors"
"github.com/stackshy/cloudemu/v2/server/wire/azurearm"
lbdriver "github.com/stackshy/cloudemu/v2/services/loadbalancer/driver"
netdriver "github.com/stackshy/cloudemu/v2/services/networking/driver"
)

Expand Down Expand Up @@ -157,6 +159,12 @@ func (h *Handler) createNIC(w http.ResponseWriter, r *http.Request, rp azurearm.

ipConfigs, err := h.buildIPConfigs(r.Context(), rp.ResourceGroup, rp.ResourceName, req.Properties.IPConfigurations)
if err != nil {
var refErr *refError
if errors.As(err, &refErr) {
azurearm.WriteError(w, http.StatusBadRequest, "InvalidResourceReference", refErr.Error())
return
}

if cerrors.IsFailedPrecondition(err) {
azurearm.WriteError(w, http.StatusBadRequest, "PublicIPAddressCannotBeAssignedToMultipleIpConfigs", cerrors.Message(err))
return
Expand Down Expand Up @@ -279,6 +287,10 @@ func (h *Handler) buildIPConfigs(
ASGIDs: refIDs(p.ApplicationSecurityGroups),
}

if err := h.checkPoolRefs(ctx, cfg.LBBackendPoolIDs); err != nil {
return nil, err
}

if p.Subnet != nil {
cfg.SubnetID = p.Subnet.ID

Expand Down Expand Up @@ -550,3 +562,61 @@ func toNICResponse(nic *netdriver.AzureNIC, rp azurearm.ResourcePath) nicRespons

return out
}

// checkPoolRefs verifies every referenced load balancer backend pool exists.
// It is a no-op when no load balancer lookup is wired.
func (h *Handler) checkPoolRefs(ctx context.Context, poolIDs []string) error {
if h.lbs == nil {
return nil
}

for _, id := range poolIDs {
rp, ok := azurearm.ParsePath(id)
if !ok || !strings.EqualFold(rp.ResourceType, "loadBalancers") ||
!strings.EqualFold(rp.SubResource, "backendAddressPools") || rp.SubResourceName == "" {
return cerrors.Newf(cerrors.InvalidArgument, "malformed loadBalancerBackendAddressPools id %q", id)
}

lb, err := h.lbs.GetAzureLoadBalancer(ctx, rp.ResourceGroup, rp.ResourceName)
if err != nil || !hasPool(lb.BackendPools, rp.SubResourceName) {
return &refError{id: id}
}
}

return nil
}

func hasPool(pools []string, name string) bool {
for _, p := range pools {
if strings.EqualFold(p, name) {
return true
}
}

return false
}

// refError marks a reference to a resource that does not exist.
type refError struct{ id string }

func (e *refError) Error() string {
return "Resource " + e.id + " referenced by resource was not found."
}

// VNetExists reports whether the virtual network with the given ARM id exists.
func (h *Handler) VNetExists(ctx context.Context, id string) bool {
rp, ok := azurearm.ParsePath(id)
if !ok || !strings.EqualFold(rp.ResourceType, typeVNet) {
return false
}

_, err := findVNetInGroup(ctx, h.net, rp.ResourceGroup, rp.ResourceName)

return err == nil
}

// SetLBResolver wires the load balancer lookup used to validate NIC backend
// pool references.
func (h *Handler) SetLBResolver(lbs lbdriver.AzureLoadBalancers) {
h.lbs = lbs
}
74 changes: 74 additions & 0 deletions server/azure/vnet/nic_pool_reference_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package vnet_test

import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/stackshy/cloudemu/v2"
azureserver "github.com/stackshy/cloudemu/v2/server/azure"
)

func rawPut(t *testing.T, ts *httptest.Server, path, body string) (int, string) {
t.Helper()

req, err := http.NewRequestWithContext(context.Background(), http.MethodPut,
ts.URL+path+"?api-version=2023-09-01", strings.NewReader(body))
if err != nil {
t.Fatal(err)
}

req.Header.Set("Content-Type", "application/json")

resp, err := ts.Client().Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()

data, _ := io.ReadAll(resp.Body)

return resp.StatusCode, string(data)
}

func TestNICBackendPoolReferenceValidated(t *testing.T) {
p := cloudemu.NewAzure()
ts := httptest.NewTLSServer(azureserver.New(azureserver.Drivers{Network: p.VNet, LB: p.LB}))
t.Cleanup(ts.Close)

ensureRG(t, ts, "sub-1", "rg-1")

base := "/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.Network/"

vnetBody := `{"location":"eastus","properties":{"addressSpace":{"addressPrefixes":["10.0.0.0/16"]},` +
`"subnets":[{"name":"s1","properties":{"addressPrefix":"10.0.1.0/24"}}]}}`
if code, body := rawPut(t, ts, base+"virtualNetworks/vn1", vnetBody); code/100 != 2 {
t.Fatalf("vnet: %d %s", code, body)
}

if code, body := rawPut(t, ts, base+"loadBalancers/lb1", `{"location":"eastus","sku":{"name":"Standard"}}`); code/100 != 2 {
t.Fatalf("lb: %d %s", code, body)
}

if code, body := rawPut(t, ts, base+"loadBalancers/lb1/backendAddressPools/pool1", `{}`); code/100 != 2 {
t.Fatalf("pool: %d %s", code, body)
}

nic := func(pool string) string {
return `{"location":"eastus","properties":{"ipConfigurations":[{"name":"ip1","properties":{` +
`"subnet":{"id":"` + base + `virtualNetworks/vn1/subnets/s1"},` +
`"loadBalancerBackendAddressPools":[{"id":"` + base + `loadBalancers/lb1/backendAddressPools/` + pool + `"}]}}]}}`
}

code, body := rawPut(t, ts, base+"networkInterfaces/nic-bad", nic("ghost"))
if code != http.StatusBadRequest || !strings.Contains(body, "InvalidResourceReference") {
t.Fatalf("missing pool: %d %s, want 400 InvalidResourceReference", code, body)
}

if code, body = rawPut(t, ts, base+"networkInterfaces/nic-ok", nic("pool1")); code/100 != 2 {
t.Fatalf("existing pool: %d %s", code, body)
}
}
Loading