v2/api/network/customizations/virtual_network_extensions.go (144 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package customizations
import (
"context"
"encoding/json"
"net/http"
"reflect"
. "github.com/Azure/azure-service-operator/v2/internal/logging"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/go-logr/logr"
"github.com/rotisserie/eris"
"sigs.k8s.io/controller-runtime/pkg/conversion"
network "github.com/Azure/azure-service-operator/v2/api/network/v1api20240301/storage"
"github.com/Azure/azure-service-operator/v2/internal/genericarmclient"
"github.com/Azure/azure-service-operator/v2/internal/resolver"
"github.com/Azure/azure-service-operator/v2/internal/util/kubeclient"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/extensions"
)
// Attention: A lot of code in this file is very similar to the logic in network_security_group_extension.go, load_balancer_extensions.go, route_table_extensions.go and compute/vmss_extensions.go.
// The two should be kept in sync as much as possible.
var _ extensions.ARMResourceModifier = &VirtualNetworkExtension{}
func (extension *VirtualNetworkExtension) ModifyARMResource(
ctx context.Context,
armClient *genericarmclient.GenericClient,
armObj genruntime.ARMResource,
obj genruntime.ARMMetaObject,
kubeClient kubeclient.Client,
resolver *resolver.Resolver,
log logr.Logger,
) (genruntime.ARMResource, error) {
typedObj, ok := obj.(*network.VirtualNetwork)
if !ok {
return nil, eris.Errorf("cannot run on unknown resource type %T, expected *network.VirtualNetwork", obj)
}
// Type assert that we are the hub type. This will fail to compile if
// the hub type has been changed but this extension has not been updated to match
var _ conversion.Hub = typedObj
resourceID, hasResourceID := genruntime.GetResourceID(obj)
if !hasResourceID {
// If we don't have an ARM ID yet, we've not been claimed so just return armObj as is
return armObj, nil
}
apiVersion, err := genruntime.GetAPIVersion(typedObj, kubeClient.Scheme())
if err != nil {
return nil, eris.Wrapf(err, "error getting api version for resource %s while getting status", obj.GetName())
}
// Get the raw resource
raw := make(map[string]any)
_, err = armClient.GetByID(ctx, resourceID, apiVersion, &raw)
if err != nil {
// If the error is NotFound, the resource we're trying to Create doesn't exist and so no modification is needed
var responseError *azcore.ResponseError
if eris.As(err, &responseError) && responseError.StatusCode == http.StatusNotFound {
return armObj, nil
}
return nil, eris.Wrapf(err, "getting resource with ID: %q", resourceID)
}
azureSubnets, err := getRawChildCollection(raw, "subnets")
if err != nil {
return nil, eris.Wrapf(err, "failed to get subnets")
}
log.V(Info).Info("Found subnets to include on VNET", "count", len(azureSubnets), "names", genruntime.RawNames(azureSubnets))
err = setChildCollection(armObj.Spec(), azureSubnets, "Subnets")
if err != nil {
return nil, eris.Wrapf(err, "failed to set subnets")
}
return armObj, nil
}
func getChildCollectionField(parent any, childFieldName string) (ret reflect.Value, err error) {
defer func() {
if x := recover(); x != nil {
err = eris.Errorf("caught panic: %s", x)
}
}()
// Here be dragons
parentValue := reflect.ValueOf(parent)
parentValue = reflect.Indirect(parentValue)
if !parentValue.IsValid() {
return reflect.Value{}, eris.Errorf("cannot assign to nil parent")
}
propertiesField := parentValue.FieldByName("Properties")
if !propertiesField.IsValid() {
return reflect.Value{}, eris.Errorf("couldn't find properties field")
}
propertiesValue := reflect.Indirect(propertiesField)
if !propertiesValue.IsValid() {
// If the properties field is nil, we must construct an entirely new properties and assign it here
temp := reflect.New(propertiesField.Type().Elem())
propertiesField.Set(temp)
propertiesValue = reflect.Indirect(temp)
}
childField := propertiesValue.FieldByName(childFieldName)
if !childField.IsValid() {
return reflect.Value{}, eris.Errorf("couldn't find %q field", childFieldName)
}
if childField.Type().Kind() != reflect.Slice {
return reflect.Value{}, eris.Errorf("%q field was not of kind Slice", childFieldName)
}
return childField, nil
}
func getRawChildCollection(parent map[string]any, childJSONName string) ([]any, error) {
props, ok := parent["properties"]
if !ok {
return nil, eris.Errorf("couldn't find properties field")
}
propsMap, ok := props.(map[string]any)
if !ok {
return nil, eris.Errorf("properties field wasn't a map")
}
childField, ok := propsMap[childJSONName]
if !ok {
return nil, eris.Errorf("couldn't find %q field", childJSONName)
}
childSlice, ok := childField.([]any)
if !ok {
return nil, eris.Errorf("%q field wasn't a slice", childJSONName)
}
return childSlice, nil
}
func setChildCollection(parent genruntime.ARMResourceSpec, childCollectionFromAzure []any, childFieldName string) (err error) {
defer func() {
if x := recover(); x != nil {
err = eris.Errorf("caught panic: %s", x)
}
}()
childField, err := getChildCollectionField(parent, childFieldName)
if err != nil {
return err
}
elemType := childField.Type().Elem()
childSlice := reflect.MakeSlice(childField.Type(), 0, 0)
for _, child := range childCollectionFromAzure {
embeddedResource := reflect.New(elemType)
err = fuzzySetResource(child, embeddedResource)
if err != nil {
return err
}
childSlice = reflect.Append(childSlice, reflect.Indirect(embeddedResource))
}
childField.Set(childSlice)
return nil
}
func fuzzySetResource(resource any, embeddedResource reflect.Value) error {
resourceJSON, err := json.Marshal(resource)
if err != nil {
return eris.Wrap(err, "failed to marshal resource JSON")
}
err = json.Unmarshal(resourceJSON, embeddedResource.Interface())
if err != nil {
return eris.Wrap(err, "failed to unmarshal resource JSON")
}
// TODO: Can't do a trivial fuzzyEqualityComparison here because we don't know which fields are readonly
// TODO: and which are not. This results in mismatches like dropping etag and other fields.
return nil
}