Skip to content

Commit

Permalink
Add patches fields for AKS resources
Browse files Browse the repository at this point in the history
  • Loading branch information
nojnhuh committed Feb 21, 2024
1 parent 37d5c1a commit 8ee2f85
Show file tree
Hide file tree
Showing 19 changed files with 512 additions and 5 deletions.
14 changes: 14 additions & 0 deletions api/v1beta1/types_class.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,13 @@ type AzureManagedControlPlaneClassSpec struct {
// SecurityProfile defines the security profile for cluster.
// +optional
SecurityProfile *ManagedClusterSecurityProfile `json:"securityProfile,omitempty"`

// ASOManagedClusterPatches defines JSON merge patches to be applied to the generated ASO ManagedCluster resource.
// WARNING: This is meant to be used sparingly to enable features for development and testing that are not
// otherwise represented in the CAPZ API. Misconfiguration that conflicts with CAPZ's normal mode of
// operation is possible.
// +optional
ASOManagedClusterPatches []string `json:"asoManagedClusterPatches,omitempty"`
}

// ManagedClusterAutoUpgradeProfile defines the auto upgrade profile for a managed cluster.
Expand Down Expand Up @@ -393,6 +400,13 @@ type AzureManagedMachinePoolClassSpec struct {
// [AKS doc]: https://learn.microsoft.com/en-us/azure/aks/enable-host-encryption
// +optional
EnableEncryptionAtHost *bool `json:"enableEncryptionAtHost,omitempty"`

// ASOManagedClustersAgentPoolPatches defines JSON merge patches to be applied to the generated ASO ManagedClustersAgentPool resource.
// WARNING: This is meant to be used sparingly to enable features for development and testing that are not
// otherwise represented in the CAPZ API. Misconfiguration that conflicts with CAPZ's normal mode of
// operation is possible.
// +optional
ASOManagedClustersAgentPoolPatches []string `json:"asoManagedClustersAgentPoolPatches,omitempty"`
}

// ManagedControlPlaneVirtualNetworkClassSpec defines the ManagedControlPlaneVirtualNetwork properties that may be shared across several managed control plane vnets.
Expand Down
10 changes: 10 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions azure/scope/managedcontrolplane.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ func (s *ManagedControlPlaneScope) ManagedClusterSpec() azure.ASOResourceSpecGet
KubeletUserAssignedIdentity: s.ControlPlane.Spec.KubeletUserAssignedIdentity,
NetworkPluginMode: s.ControlPlane.Spec.NetworkPluginMode,
DNSPrefix: s.ControlPlane.Spec.DNSPrefix,
Patches: s.ControlPlane.Spec.ASOManagedClusterPatches,
}

if s.ControlPlane.Spec.SSHPublicKey != nil {
Expand Down
1 change: 1 addition & 0 deletions azure/scope/managedmachinepool.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ func buildAgentPoolSpec(managedControlPlane *infrav1.AzureManagedControlPlane,
LinuxOSConfig: managedMachinePool.Spec.LinuxOSConfig,
EnableFIPS: managedMachinePool.Spec.EnableFIPS,
EnableEncryptionAtHost: managedMachinePool.Spec.EnableEncryptionAtHost,
Patches: managedMachinePool.Spec.ASOManagedClustersAgentPoolPatches,
}

if managedMachinePool.Spec.OSDiskSizeGB != nil {
Expand Down
11 changes: 11 additions & 0 deletions azure/services/agentpools/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"k8s.io/utils/ptr"
infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
"sigs.k8s.io/cluster-api-provider-azure/azure"
"sigs.k8s.io/cluster-api-provider-azure/azure/services/aso"
"sigs.k8s.io/cluster-api-provider-azure/util/tele"
"sigs.k8s.io/cluster-api-provider-azure/util/versions"
)
Expand Down Expand Up @@ -150,6 +151,9 @@ type AgentPoolSpec struct {

// EnableEncryptionAtHost indicates whether host encryption is enabled on the node pool
EnableEncryptionAtHost *bool

// Patches are extra patches to be applied to the ASO resource.
Patches []string
}

// ResourceRef implements azure.ASOResourceSpecGetter.
Expand Down Expand Up @@ -306,3 +310,10 @@ func (s *AgentPoolSpec) WasManaged(resource *asocontainerservicev1.ManagedCluste
// CAPZ has never supported BYO agent pools.
return true
}

var _ aso.Patcher = (*AgentPoolSpec)(nil)

// ExtraPatches implements aso.Patcher.
func (s *AgentPoolSpec) ExtraPatches() []string {
return s.Patches

Check warning on line 318 in azure/services/agentpools/spec.go

View check run for this annotation

Codecov / codecov/patch

azure/services/agentpools/spec.go#L317-L318

Added lines #L317 - L318 were not covered by tests
}
65 changes: 62 additions & 3 deletions azure/services/aso/aso.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,22 @@ package aso

import (
"context"
"encoding/json"
"fmt"
"time"

asoannotations "github.com/Azure/azure-service-operator/v2/pkg/common/annotations"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/conditions"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/yaml"
infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
"sigs.k8s.io/cluster-api-provider-azure/azure"
"sigs.k8s.io/cluster-api-provider-azure/util/aso"
Expand All @@ -53,6 +58,7 @@ const (
type deepCopier[T any] interface {
genruntime.MetaObject
DeepCopy() T
SetGroupVersionKind(schema.GroupVersionKind)
}

// reconciler is an implementation of the Reconciler interface. It handles creation
Expand Down Expand Up @@ -150,7 +156,7 @@ func (r *reconciler[T]) CreateOrUpdateResource(ctx context.Context, spec azure.A
}

// Construct parameters using the resource spec and information from the existing resource, if there is one.
parameters, err := spec.Parameters(ctx, existing.DeepCopy())
parameters, err := PatchedParameters(ctx, r.Scheme(), spec, existing.DeepCopy())
if err != nil {
return zero, errors.Wrapf(err, "failed to get desired parameters for resource %s/%s (service: %s)", resourceNamespace, resourceName, serviceName)
}
Expand Down Expand Up @@ -220,11 +226,64 @@ func (r *reconciler[T]) CreateOrUpdateResource(ctx context.Context, spec azure.A
log.V(2).Info("resource up to date")
return existing, nil
}
log.V(2).Info("creating or updating resource", "diff", cmp.Diff(existing, parameters))
log.V(2).Info("creating or updating resource", "diff", diff)
return r.createOrUpdateResource(ctx, existing, parameters, resourceExists, serviceName)
}

func (r *reconciler[T]) createOrUpdateResource(ctx context.Context, existing T, parameters T, resourceExists bool, serviceName string) (T, error) {
// PatchedParameters returns the Parameters of spec with patches applied.
func PatchedParameters[T deepCopier[T]](ctx context.Context, scheme *runtime.Scheme, spec azure.ASOResourceSpecGetter[T], existing T) (T, error) {
var zero T // to be returned with non-nil errors
parameters, err := spec.Parameters(ctx, existing)
if err != nil {
return zero, err
}
return applyPatches(scheme, spec, parameters)
}

func applyPatches[T deepCopier[T]](scheme *runtime.Scheme, spec azure.ASOResourceSpecGetter[T], parameters T) (T, error) {
p, ok := spec.(Patcher)
if !ok {
return parameters, nil
}

var zero T // to be returned with non-nil errors

gvk, err := apiutil.GVKForObject(parameters, scheme)
if err != nil {
return zero, errors.Wrap(err, "failed to get GroupVersionKind for object")

Check warning on line 253 in azure/services/aso/aso.go

View check run for this annotation

Codecov / codecov/patch

azure/services/aso/aso.go#L253

Added line #L253 was not covered by tests
}
parameters.SetGroupVersionKind(gvk)
paramData, err := json.Marshal(parameters)
if err != nil {
return zero, errors.Wrap(err, "failed to marshal JSON for patch")

Check warning on line 258 in azure/services/aso/aso.go

View check run for this annotation

Codecov / codecov/patch

azure/services/aso/aso.go#L258

Added line #L258 was not covered by tests
}

for i, extraPatch := range p.ExtraPatches() {
jsonPatch, err := yaml.ToJSON([]byte(extraPatch))
if err != nil {
return zero, errors.Wrapf(err, "failed to convert patch at index %d to JSON", i)

Check warning on line 264 in azure/services/aso/aso.go

View check run for this annotation

Codecov / codecov/patch

azure/services/aso/aso.go#L264

Added line #L264 was not covered by tests
}
paramData, err = jsonpatch.MergePatch(paramData, jsonPatch)
if err != nil {
return zero, errors.Wrapf(err, "failed to apply patch at index %d", i)

Check warning on line 268 in azure/services/aso/aso.go

View check run for this annotation

Codecov / codecov/patch

azure/services/aso/aso.go#L268

Added line #L268 was not covered by tests
}
}

decoder := serializer.NewCodecFactory(scheme).UniversalDeserializer()
obj, _, err := decoder.Decode(paramData, nil, nil)
if err != nil {
return zero, errors.Wrap(err, "failed to decode object")

Check warning on line 275 in azure/services/aso/aso.go

View check run for this annotation

Codecov / codecov/patch

azure/services/aso/aso.go#L275

Added line #L275 was not covered by tests
}

t, ok := obj.(T)
if !ok {
return zero, fmt.Errorf("decoded patched object is %T, not %T", obj, parameters)

Check warning on line 280 in azure/services/aso/aso.go

View check run for this annotation

Codecov / codecov/patch

azure/services/aso/aso.go#L280

Added line #L280 was not covered by tests
}

return t, nil
}

func (r *reconciler[T]) createOrUpdateResource(ctx context.Context, existing T, parameters client.Object, resourceExists bool, serviceName string) (T, error) {
var zero T
var err error
var logMessageVerbPrefix string
Expand Down
119 changes: 119 additions & 0 deletions azure/services/aso/aso_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,125 @@ func TestCreateOrUpdateResource(t *testing.T) {
g.Expect(updated.Annotations).NotTo(HaveKey(prePauseReconcilePolicyAnnotation))
g.Expect(updated.Annotations).To(HaveKeyWithValue(asoannotations.ReconcilePolicy, string(asoannotations.ReconcilePolicyManage)))
})

t.Run("patches applied on create", func(t *testing.T) {
g := NewGomegaWithT(t)

sch := runtime.NewScheme()
g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed())
c := fakeclient.NewClientBuilder().
WithScheme(sch).
Build()
s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner())

mockCtrl := gomock.NewController(t)
specMock := struct {
*mock_azure.MockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]
*mock_aso.MockPatcher
}{
mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
mock_aso.NewMockPatcher(mockCtrl),
}
specMock.MockASOResourceSpecGetter.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
},
})
specMock.MockASOResourceSpecGetter.EXPECT().Parameters(gomockinternal.AContext(), gomock.Any()).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) {
return &asoresourcesv1.ResourceGroup{
Spec: asoresourcesv1.ResourceGroup_Spec{
Location: ptr.To("location-from-parameters"),
},
}, nil
})

specMock.MockPatcher.EXPECT().ExtraPatches().Return([]string{
`{"metadata": {"labels": {"extra-patch": "not-this-value"}}}`,
`{"metadata": {"labels": {"extra-patch": "this-value"}}}`,
`{"metadata": {"labels": {"another": "label"}}}`,
})

ctx := context.Background()

result, err := s.CreateOrUpdateResource(ctx, specMock, "service")
g.Expect(result).To(BeNil())
g.Expect(azure.IsOperationNotDoneError(err)).To(BeTrue(), "expected not done error, got %v", err)

updated := &asoresourcesv1.ResourceGroup{}
g.Expect(c.Get(ctx, types.NamespacedName{Name: "name", Namespace: "namespace"}, updated)).To(Succeed())
g.Expect(updated.Labels).To(HaveKeyWithValue("extra-patch", "this-value"))
g.Expect(updated.Labels).To(HaveKeyWithValue("another", "label"))
g.Expect(*updated.Spec.Location).To(Equal("location-from-parameters"))
})

t.Run("patches applied on update", func(t *testing.T) {
g := NewGomegaWithT(t)

sch := runtime.NewScheme()
g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed())
c := fakeclient.NewClientBuilder().
WithScheme(sch).
Build()
s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner())

mockCtrl := gomock.NewController(t)
specMock := struct {
*mock_azure.MockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]
*mock_aso.MockPatcher
}{
mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
mock_aso.NewMockPatcher(mockCtrl),
}
specMock.MockASOResourceSpecGetter.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
},
})
specMock.MockASOResourceSpecGetter.EXPECT().Parameters(gomockinternal.AContext(), gomock.Any()).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) {
group.Spec.Location = ptr.To("location-from-parameters")
return group, nil
})
specMock.MockASOResourceSpecGetter.EXPECT().WasManaged(gomock.Any()).Return(false)

specMock.MockPatcher.EXPECT().ExtraPatches().Return([]string{
`{"metadata": {"labels": {"extra-patch": "not-this-value"}}}`,
`{"metadata": {"labels": {"extra-patch": "this-value"}}}`,
`{"metadata": {"labels": {"another": "label"}}}`,
})

ctx := context.Background()
g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
OwnerReferences: ownerRefs(),
Annotations: map[string]string{
asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicyManage),
},
},
Spec: asoresourcesv1.ResourceGroup_Spec{
Location: ptr.To("location"),
},
Status: asoresourcesv1.ResourceGroup_STATUS{
Conditions: []conditions.Condition{
{
Type: conditions.ConditionTypeReady,
Status: metav1.ConditionTrue,
},
},
},
})).To(Succeed())

result, err := s.CreateOrUpdateResource(ctx, specMock, "service")
g.Expect(result).To(BeNil())
g.Expect(azure.IsOperationNotDoneError(err)).To(BeTrue(), "expected not done error, got %v", err)

updated := &asoresourcesv1.ResourceGroup{}
g.Expect(c.Get(ctx, types.NamespacedName{Name: "name", Namespace: "namespace"}, updated)).To(Succeed())
g.Expect(updated.Labels).To(HaveKeyWithValue("extra-patch", "this-value"))
g.Expect(updated.Labels).To(HaveKeyWithValue("another", "label"))
g.Expect(*updated.Spec.Location).To(Equal("location-from-parameters"))
})
}

// TestDeleteResource tests the DeleteResource function.
Expand Down
5 changes: 5 additions & 0 deletions azure/services/aso/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ type TagsGetterSetter[T genruntime.MetaObject] interface {
SetTags(resource T, tags infrav1.Tags)
}

// Patcher supplies extra patches to be applied to an ASO resource.
type Patcher interface {
ExtraPatches() []string
}

// Scope represents the common functionality related to all scopes needed for ASO services.
type Scope interface {
azure.AsyncStatusUpdater
Expand Down
37 changes: 37 additions & 0 deletions azure/services/aso/mock_aso/aso_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 8ee2f85

Please sign in to comment.