diff --git a/api/v1alpha1/aws_clusterconfig_types.go b/api/v1alpha1/aws_clusterconfig_types.go index e0e113b60..2082248f2 100644 --- a/api/v1alpha1/aws_clusterconfig_types.go +++ b/api/v1alpha1/aws_clusterconfig_types.go @@ -5,6 +5,9 @@ package v1alpha1 import ( clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + + "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/capi/clustertopology/variables" + capav1 "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/external/sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" ) type AWSSpec struct { @@ -13,6 +16,8 @@ type AWSSpec struct { Region *Region `json:"region,omitempty"` // +optional Network *AWSNetwork `json:"network,omitempty"` + // +optional + ControlPlaneLoadBalancer *AWSLoadBalancerSpec `json:"controlPlaneLoadBalancer,omitempty"` } func (AWSSpec) VariableSchema() clusterv1.VariableSchema { @@ -21,8 +26,9 @@ func (AWSSpec) VariableSchema() clusterv1.VariableSchema { Description: "AWS cluster configuration", Type: "object", Properties: map[string]clusterv1.JSONSchemaProps{ - "region": Region("").VariableSchema().OpenAPIV3Schema, - "network": AWSNetwork{}.VariableSchema().OpenAPIV3Schema, + "region": Region("").VariableSchema().OpenAPIV3Schema, + "network": AWSNetwork{}.VariableSchema().OpenAPIV3Schema, + "controlPlaneLoadBalancer": AWSLoadBalancerSpec{}.VariableSchema().OpenAPIV3Schema, }, }, } @@ -114,3 +120,30 @@ func (SubnetSpec) VariableSchema() clusterv1.VariableSchema { }, } } + +// AWSLoadBalancerSpec configures an AWS control-plane LoadBalancer. +type AWSLoadBalancerSpec struct { + // Scheme sets the scheme of the load balancer (defaults to internet-facing) + // +kubebuilder:default=internet-facing + // +kubebuilder:validation:Enum=internet-facing;internal + // +optional + Scheme *capav1.ELBScheme `json:"scheme,omitempty"` +} + +func (AWSLoadBalancerSpec) VariableSchema() clusterv1.VariableSchema { + supportedScheme := []capav1.ELBScheme{capav1.ELBSchemeInternetFacing, capav1.ELBSchemeInternal} + + return clusterv1.VariableSchema{ + OpenAPIV3Schema: clusterv1.JSONSchemaProps{ + Description: "AWS control-plane LoadBalancer configuration", + Type: "object", + Properties: map[string]clusterv1.JSONSchemaProps{ + "scheme": { + Description: "Scheme sets the scheme of the load balancer (defaults to internet-facing)", + Type: "string", + Enum: variables.MustMarshalValuesToEnumJSON(supportedScheme...), + }, + }, + }, + } +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e34069e87..e65c3d90a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -8,7 +8,8 @@ package v1alpha1 import ( - "k8s.io/api/core/v1" + "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/external/sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" + v1 "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -47,6 +48,26 @@ func (in *AMISpec) DeepCopy() *AMISpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSLoadBalancerSpec) DeepCopyInto(out *AWSLoadBalancerSpec) { + *out = *in + if in.Scheme != nil { + in, out := &in.Scheme, &out.Scheme + *out = new(v1beta2.ELBScheme) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSLoadBalancerSpec. +func (in *AWSLoadBalancerSpec) DeepCopy() *AWSLoadBalancerSpec { + if in == nil { + return nil + } + out := new(AWSLoadBalancerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AWSNetwork) DeepCopyInto(out *AWSNetwork) { *out = *in @@ -115,6 +136,11 @@ func (in *AWSSpec) DeepCopyInto(out *AWSSpec) { *out = new(AWSNetwork) (*in).DeepCopyInto(*out) } + if in.ControlPlaneLoadBalancer != nil { + in, out := &in.ControlPlaneLoadBalancer, &out.ControlPlaneLoadBalancer + *out = new(AWSLoadBalancerSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSSpec. diff --git a/docs/content/customization/aws/controlplaneloadbalancer.md b/docs/content/customization/aws/controlplaneloadbalancer.md new file mode 100644 index 000000000..31ae2061b --- /dev/null +++ b/docs/content/customization/aws/controlplaneloadbalancer.md @@ -0,0 +1,38 @@ ++++ +title = "Control Plane Load Balancer" ++++ + +The control-plane load balancer customization allows the user +to modify the load balancer configuration for the control-plane's API server. + +This customization will be available when the +[provider-specific cluster configuration patch]({{< ref "..">}}) is included in the `ClusterClass`. + +## Example + +To use an internal ELB scheme, use the following configuration: + +```yaml +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: +spec: + topology: + variables: + - name: clusterConfig + value: + aws: + controlPlaneLoadBalancer: + scheme: internal +``` + +Applying this configuration will result in the following value being set: + +- `AWSClusterTemplate`: + + - ```yaml + spec: + controlPlaneLoadBalancer: + scheme: internal + ``` diff --git a/pkg/handlers/aws/mutation/controlplaneloadbalancer/inject.go b/pkg/handlers/aws/mutation/controlplaneloadbalancer/inject.go new file mode 100644 index 000000000..6df547fa3 --- /dev/null +++ b/pkg/handlers/aws/mutation/controlplaneloadbalancer/inject.go @@ -0,0 +1,105 @@ +// Copyright 2023 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package controlplaneloadbalancer + +import ( + "context" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/d2iq-labs/capi-runtime-extensions/api/v1alpha1" + "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/capi/clustertopology/patches" + "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/capi/clustertopology/patches/selectors" + "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/capi/clustertopology/variables" + capav1 "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/external/sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" + "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/generic/clusterconfig" +) + +const ( + // VariableName is the external patch variable name. + VariableName = "controlPlaneLoadBalancer" +) + +type awsControlPlaneLoadBalancer struct { + variableName string + variableFieldPath []string +} + +func NewPatch() *awsControlPlaneLoadBalancer { + return newAWSControlPlaneLoadBalancer( + clusterconfig.MetaVariableName, + v1alpha1.AWSVariableName, + VariableName, + ) +} + +func newAWSControlPlaneLoadBalancer( + variableName string, + variableFieldPath ...string, +) *awsControlPlaneLoadBalancer { + return &awsControlPlaneLoadBalancer{ + variableName: variableName, + variableFieldPath: variableFieldPath, + } +} + +func (h *awsControlPlaneLoadBalancer) Mutate( + ctx context.Context, + obj *unstructured.Unstructured, + vars map[string]apiextensionsv1.JSON, + holderRef runtimehooksv1.HolderReference, + _ client.ObjectKey, +) error { + log := ctrl.LoggerFrom(ctx).WithValues( + "holderRef", holderRef, + ) + + controlPlaneLoadBalancerVar, found, err := variables.Get[v1alpha1.AWSLoadBalancerSpec]( + vars, + h.variableName, + h.variableFieldPath..., + ) + if err != nil { + return err + } + if !found { + log.V(5).Info("AWS ControlPlaneLoadBalancer variable not defined") + return nil + } + + log = log.WithValues( + "variableName", + h.variableName, + "variableFieldPath", + h.variableFieldPath, + "variableValue", + controlPlaneLoadBalancerVar, + ) + + return patches.MutateIfApplicable( + obj, + vars, + &holderRef, + selectors.InfrastructureCluster(capav1.GroupVersion.Version, "AWSClusterTemplate"), + log, + func(obj *capav1.AWSClusterTemplate) error { + log.WithValues( + "patchedObjectKind", obj.GetObjectKind().GroupVersionKind().String(), + "patchedObjectName", client.ObjectKeyFromObject(obj), + ).Info("setting ControlPlaneLoadBalancer in AWSCluster spec") + + controlPlaneLoadBalancer := obj.Spec.Template.Spec.ControlPlaneLoadBalancer + if controlPlaneLoadBalancer == nil { + obj.Spec.Template.Spec.ControlPlaneLoadBalancer = &capav1.AWSLoadBalancerSpec{} + } + obj.Spec.Template.Spec.ControlPlaneLoadBalancer.Scheme = controlPlaneLoadBalancerVar.Scheme + + return nil + }, + ) +} diff --git a/pkg/handlers/aws/mutation/controlplaneloadbalancer/tests/generate_patches.go b/pkg/handlers/aws/mutation/controlplaneloadbalancer/tests/generate_patches.go new file mode 100644 index 000000000..3b9c73ba4 --- /dev/null +++ b/pkg/handlers/aws/mutation/controlplaneloadbalancer/tests/generate_patches.go @@ -0,0 +1,74 @@ +// Copyright 2023 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package tests + +import ( + "testing" + + "github.com/onsi/gomega" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + + "github.com/d2iq-labs/capi-runtime-extensions/api/v1alpha1" + "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/capi/clustertopology/handlers/mutation" + capav1 "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/external/sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" + "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/testutils/capitest" + "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/testutils/capitest/request" +) + +func TestGeneratePatches( + t *testing.T, + generatorFunc func() mutation.GeneratePatches, + variableName string, + variablePath ...string, +) { + t.Helper() + + capitest.ValidateGeneratePatches( + t, + generatorFunc, + capitest.PatchTestDef{ + Name: "unset variable", + }, + capitest.PatchTestDef{ + Name: "ControlPlaneLoadbalancer scheme set to internet-facing", + Vars: []runtimehooksv1.Variable{ + capitest.VariableWithValue( + variableName, + v1alpha1.AWSLoadBalancerSpec{ + Scheme: &capav1.ELBSchemeInternetFacing, + }, + variablePath..., + ), + }, + RequestItem: request.NewAWSClusterTemplateRequestItem("1234"), + ExpectedPatchMatchers: []capitest.JSONPatchMatcher{{ + Operation: "add", + Path: "/spec/template/spec/controlPlaneLoadBalancer", + ValueMatcher: gomega.HaveKeyWithValue( + "scheme", "internet-facing", + ), + }}, + }, + capitest.PatchTestDef{ + Name: "ControlPlaneLoadbalancer scheme set to internal", + Vars: []runtimehooksv1.Variable{ + capitest.VariableWithValue( + variableName, + v1alpha1.AWSLoadBalancerSpec{ + Scheme: &capav1.ELBSchemeInternal, + }, + variablePath..., + ), + }, + RequestItem: request.NewAWSClusterTemplateRequestItem("1234"), + ExpectedPatchMatchers: []capitest.JSONPatchMatcher{{ + Operation: "add", + Path: "/spec/template/spec/controlPlaneLoadBalancer", + ValueMatcher: gomega.HaveKeyWithValue( + "scheme", "internal", + ), + }}, + }, + ) +} diff --git a/pkg/handlers/aws/mutation/controlplaneloadbalancer/variables_test.go b/pkg/handlers/aws/mutation/controlplaneloadbalancer/variables_test.go new file mode 100644 index 000000000..5c0391d6e --- /dev/null +++ b/pkg/handlers/aws/mutation/controlplaneloadbalancer/variables_test.go @@ -0,0 +1,57 @@ +// Copyright 2023 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package controlplaneloadbalancer + +import ( + "testing" + + "k8s.io/utils/ptr" + + "github.com/d2iq-labs/capi-runtime-extensions/api/v1alpha1" + capav1 "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/external/sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" + "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/testutils/capitest" + awsclusterconfig "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/clusterconfig" + "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/generic/clusterconfig" +) + +func TestVariableValidation(t *testing.T) { + capitest.ValidateDiscoverVariables( + t, + clusterconfig.MetaVariableName, + ptr.To(v1alpha1.ClusterConfigSpec{AWS: &v1alpha1.AWSSpec{}}.VariableSchema()), + true, + awsclusterconfig.NewVariable, + capitest.VariableTestDef{ + Name: "specified internet-facing scheme", + Vals: v1alpha1.ClusterConfigSpec{ + AWS: &v1alpha1.AWSSpec{ + ControlPlaneLoadBalancer: &v1alpha1.AWSLoadBalancerSpec{ + Scheme: &capav1.ELBSchemeInternetFacing, + }, + }, + }, + }, + capitest.VariableTestDef{ + Name: "specified internal scheme", + Vals: v1alpha1.ClusterConfigSpec{ + AWS: &v1alpha1.AWSSpec{ + ControlPlaneLoadBalancer: &v1alpha1.AWSLoadBalancerSpec{ + Scheme: &capav1.ELBSchemeInternal, + }, + }, + }, + }, + capitest.VariableTestDef{ + Name: "specified invalid scheme", + Vals: v1alpha1.ClusterConfigSpec{ + AWS: &v1alpha1.AWSSpec{ + ControlPlaneLoadBalancer: &v1alpha1.AWSLoadBalancerSpec{ + Scheme: ptr.To(capav1.ELBScheme("invalid")), + }, + }, + }, + ExpectError: true, + }, + ) +} diff --git a/pkg/handlers/aws/mutation/metapatch_handler.go b/pkg/handlers/aws/mutation/metapatch_handler.go index a7c2ffc4c..470518c05 100644 --- a/pkg/handlers/aws/mutation/metapatch_handler.go +++ b/pkg/handlers/aws/mutation/metapatch_handler.go @@ -10,6 +10,7 @@ import ( "github.com/d2iq-labs/capi-runtime-extensions/common/pkg/capi/clustertopology/handlers/mutation" "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/ami" "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/cni/calico" + "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/controlplaneloadbalancer" "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/iaminstanceprofile" "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/instancetype" "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/network" @@ -24,6 +25,7 @@ func MetaPatchHandler(mgr manager.Manager) handlers.Named { calico.NewPatch(), region.NewPatch(), network.NewPatch(), + controlplaneloadbalancer.NewPatch(), iaminstanceprofile.NewControlPlanePatch(), instancetype.NewControlPlanePatch(), ami.NewControlPlanePatch(), diff --git a/pkg/handlers/aws/mutation/metapatch_handler_test.go b/pkg/handlers/aws/mutation/metapatch_handler_test.go index 586c2f9dd..87a288229 100644 --- a/pkg/handlers/aws/mutation/metapatch_handler_test.go +++ b/pkg/handlers/aws/mutation/metapatch_handler_test.go @@ -16,6 +16,8 @@ import ( "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/ami" amitests "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/ami/tests" calicotests "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/cni/calico/tests" + "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/controlplaneloadbalancer" + controlplaneloadbalancertests "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/controlplaneloadbalancer/tests" "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/iaminstanceprofile" iaminstanceprofiletests "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/iaminstanceprofile/tests" "github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/aws/mutation/instancetype" @@ -180,4 +182,12 @@ func TestGeneratePatches(t *testing.T) { v1alpha1.AWSVariableName, network.VariableName, ) + + controlplaneloadbalancertests.TestGeneratePatches( + t, + metaPatchGeneratorFunc(mgr), + clusterconfig.MetaVariableName, + v1alpha1.AWSVariableName, + controlplaneloadbalancer.VariableName, + ) }