diff --git a/api/v1alpha3/azureclusteridentity_conversion.go b/api/v1alpha3/azureclusteridentity_conversion.go new file mode 100644 index 00000000000..00690911905 --- /dev/null +++ b/api/v1alpha3/azureclusteridentity_conversion.go @@ -0,0 +1,87 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha3 + +import ( + apiconversion "k8s.io/apimachinery/pkg/conversion" + infrav1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" + utilconversion "sigs.k8s.io/cluster-api/util/conversion" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// ConvertTo converts this AzureCluster to the Hub version (v1alpha4). +func (src *AzureClusterIdentity) ConvertTo(dstRaw conversion.Hub) error { // nolint + dst := dstRaw.(*infrav1alpha4.AzureClusterIdentity) + if err := Convert_v1alpha3_AzureClusterIdentity_To_v1alpha4_AzureClusterIdentity(src, dst, nil); err != nil { + return err + } + + // Manually restore data. + restored := &infrav1alpha4.AzureClusterIdentity{} + if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok { + return err + } + + if len(src.Spec.AllowedNamespaces) > 0 { + dst.Spec.AllowedNamespaces = &infrav1alpha4.AllowedNamespaces{} + for _, ns := range src.Spec.AllowedNamespaces { + dst.Spec.AllowedNamespaces.NamespaceList = append(dst.Spec.AllowedNamespaces.NamespaceList, ns) + } + dst.Spec.AllowedNamespaces.Selector = restored.Spec.AllowedNamespaces.Selector + } + + return nil +} + +// ConvertFrom converts from the Hub version (v1alpha4) to this version. +func (dst *AzureClusterIdentity) ConvertFrom(srcRaw conversion.Hub) error { // nolint + src := srcRaw.(*infrav1alpha4.AzureClusterIdentity) + if err := Convert_v1alpha4_AzureClusterIdentity_To_v1alpha3_AzureClusterIdentity(src, dst, nil); err != nil { + return err + } + + // Preserve Hub data on down-conversion. + if err := utilconversion.MarshalData(src, dst); err != nil { + return err + } + + if src.Spec.AllowedNamespaces != nil { + for _, ns := range src.Spec.AllowedNamespaces.NamespaceList { + dst.Spec.AllowedNamespaces = append(dst.Spec.AllowedNamespaces, ns) + } + } + + return nil +} + +// Convert_v1alpha3_AzureClusterIdentitySpec_To_v1alpha4_AzureClusterIdentitySpec. +func Convert_v1alpha3_AzureClusterIdentitySpec_To_v1alpha4_AzureClusterIdentitySpec(in *AzureClusterIdentitySpec, out *infrav1alpha4.AzureClusterIdentitySpec, s apiconversion.Scope) error { //nolint + if err := autoConvert_v1alpha3_AzureClusterIdentitySpec_To_v1alpha4_AzureClusterIdentitySpec(in, out, s); err != nil { + return err + } + + return nil +} + +// Convert_v1alpha4_AzureClusterIdentitySpec_To_v1alpha3_AzureClusterIdentitySpec +func Convert_v1alpha4_AzureClusterIdentitySpec_To_v1alpha3_AzureClusterIdentitySpec(in *infrav1alpha4.AzureClusterIdentitySpec, out *AzureClusterIdentitySpec, s apiconversion.Scope) error { //nolint + if err := autoConvert_v1alpha4_AzureClusterIdentitySpec_To_v1alpha3_AzureClusterIdentitySpec(in, out, s); err != nil { + return err + } + + return nil +} diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go index 5f6ebffaf22..e3f06720a94 100644 --- a/api/v1alpha3/zz_generated.conversion.go +++ b/api/v1alpha3/zz_generated.conversion.go @@ -80,16 +80,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*AzureClusterIdentitySpec)(nil), (*v1alpha4.AzureClusterIdentitySpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha3_AzureClusterIdentitySpec_To_v1alpha4_AzureClusterIdentitySpec(a.(*AzureClusterIdentitySpec), b.(*v1alpha4.AzureClusterIdentitySpec), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*v1alpha4.AzureClusterIdentitySpec)(nil), (*AzureClusterIdentitySpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha4_AzureClusterIdentitySpec_To_v1alpha3_AzureClusterIdentitySpec(a.(*v1alpha4.AzureClusterIdentitySpec), b.(*AzureClusterIdentitySpec), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*AzureClusterIdentityStatus)(nil), (*v1alpha4.AzureClusterIdentityStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha3_AzureClusterIdentityStatus_To_v1alpha4_AzureClusterIdentityStatus(a.(*AzureClusterIdentityStatus), b.(*v1alpha4.AzureClusterIdentityStatus), scope) }); err != nil { @@ -330,6 +320,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*AzureClusterIdentitySpec)(nil), (*v1alpha4.AzureClusterIdentitySpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_AzureClusterIdentitySpec_To_v1alpha4_AzureClusterIdentitySpec(a.(*AzureClusterIdentitySpec), b.(*v1alpha4.AzureClusterIdentitySpec), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*AzureClusterSpec)(nil), (*v1alpha4.AzureClusterSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha3_AzureClusterSpec_To_v1alpha4_AzureClusterSpec(a.(*AzureClusterSpec), b.(*v1alpha4.AzureClusterSpec), scope) }); err != nil { @@ -385,6 +380,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1alpha4.AzureClusterIdentitySpec)(nil), (*AzureClusterIdentitySpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha4_AzureClusterIdentitySpec_To_v1alpha3_AzureClusterIdentitySpec(a.(*v1alpha4.AzureClusterIdentitySpec), b.(*AzureClusterIdentitySpec), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1alpha4.AzureClusterSpec)(nil), (*AzureClusterSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_AzureClusterSpec_To_v1alpha3_AzureClusterSpec(a.(*v1alpha4.AzureClusterSpec), b.(*AzureClusterSpec), scope) }); err != nil { @@ -531,7 +531,17 @@ func Convert_v1alpha4_AzureClusterIdentity_To_v1alpha3_AzureClusterIdentity(in * func autoConvert_v1alpha3_AzureClusterIdentityList_To_v1alpha4_AzureClusterIdentityList(in *AzureClusterIdentityList, out *v1alpha4.AzureClusterIdentityList, s conversion.Scope) error { out.ListMeta = in.ListMeta - out.Items = *(*[]v1alpha4.AzureClusterIdentity)(unsafe.Pointer(&in.Items)) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]v1alpha4.AzureClusterIdentity, len(*in)) + for i := range *in { + if err := Convert_v1alpha3_AzureClusterIdentity_To_v1alpha4_AzureClusterIdentity(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } return nil } @@ -542,7 +552,17 @@ func Convert_v1alpha3_AzureClusterIdentityList_To_v1alpha4_AzureClusterIdentityL func autoConvert_v1alpha4_AzureClusterIdentityList_To_v1alpha3_AzureClusterIdentityList(in *v1alpha4.AzureClusterIdentityList, out *AzureClusterIdentityList, s conversion.Scope) error { out.ListMeta = in.ListMeta - out.Items = *(*[]AzureClusterIdentity)(unsafe.Pointer(&in.Items)) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AzureClusterIdentity, len(*in)) + for i := range *in { + if err := Convert_v1alpha4_AzureClusterIdentity_To_v1alpha3_AzureClusterIdentity(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } return nil } @@ -557,30 +577,20 @@ func autoConvert_v1alpha3_AzureClusterIdentitySpec_To_v1alpha4_AzureClusterIdent out.ClientID = in.ClientID out.ClientSecret = in.ClientSecret out.TenantID = in.TenantID - out.AllowedNamespaces = *(*[]string)(unsafe.Pointer(&in.AllowedNamespaces)) + // WARNING: in.AllowedNamespaces requires manual conversion: inconvertible types ([]string vs *sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4.AllowedNamespaces) return nil } -// Convert_v1alpha3_AzureClusterIdentitySpec_To_v1alpha4_AzureClusterIdentitySpec is an autogenerated conversion function. -func Convert_v1alpha3_AzureClusterIdentitySpec_To_v1alpha4_AzureClusterIdentitySpec(in *AzureClusterIdentitySpec, out *v1alpha4.AzureClusterIdentitySpec, s conversion.Scope) error { - return autoConvert_v1alpha3_AzureClusterIdentitySpec_To_v1alpha4_AzureClusterIdentitySpec(in, out, s) -} - func autoConvert_v1alpha4_AzureClusterIdentitySpec_To_v1alpha3_AzureClusterIdentitySpec(in *v1alpha4.AzureClusterIdentitySpec, out *AzureClusterIdentitySpec, s conversion.Scope) error { out.Type = IdentityType(in.Type) out.ResourceID = in.ResourceID out.ClientID = in.ClientID out.ClientSecret = in.ClientSecret out.TenantID = in.TenantID - out.AllowedNamespaces = *(*[]string)(unsafe.Pointer(&in.AllowedNamespaces)) + // WARNING: in.AllowedNamespaces requires manual conversion: inconvertible types (*sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4.AllowedNamespaces vs []string) return nil } -// Convert_v1alpha4_AzureClusterIdentitySpec_To_v1alpha3_AzureClusterIdentitySpec is an autogenerated conversion function. -func Convert_v1alpha4_AzureClusterIdentitySpec_To_v1alpha3_AzureClusterIdentitySpec(in *v1alpha4.AzureClusterIdentitySpec, out *AzureClusterIdentitySpec, s conversion.Scope) error { - return autoConvert_v1alpha4_AzureClusterIdentitySpec_To_v1alpha3_AzureClusterIdentitySpec(in, out, s) -} - func autoConvert_v1alpha3_AzureClusterIdentityStatus_To_v1alpha4_AzureClusterIdentityStatus(in *AzureClusterIdentityStatus, out *v1alpha4.AzureClusterIdentityStatus, s conversion.Scope) error { out.Conditions = *(*apiv1alpha4.Conditions)(unsafe.Pointer(&in.Conditions)) return nil diff --git a/api/v1alpha4/azureclusteridentity_conversion.go b/api/v1alpha4/azureclusteridentity_conversion.go new file mode 100644 index 00000000000..61c3d39ba83 --- /dev/null +++ b/api/v1alpha4/azureclusteridentity_conversion.go @@ -0,0 +1,23 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha4 + +// Hub marks AzureClusterIdentity as a conversion hub. +func (*AzureClusterIdentity) Hub() {} + +// Hub marks AzureClusterIdentityList as a conversion hub. +func (*AzureClusterIdentityList) Hub() {} diff --git a/api/v1alpha4/azureclusteridentity_types.go b/api/v1alpha4/azureclusteridentity_types.go index 46d11bec6f2..62fb794d957 100644 --- a/api/v1alpha4/azureclusteridentity_types.go +++ b/api/v1alpha4/azureclusteridentity_types.go @@ -22,6 +22,25 @@ import ( clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4" ) +// AllowedNamespaces defines the namespaces the clusters are allowed to use the identity from +// NamespaceList takes precedence over the Selector +type AllowedNamespaces struct { + // A nil or empty list indicates that AzureCluster cannot use the identity from any namespace. + // + // +optional + // +nullable + NamespaceList []string `json:"list"` + // Selector is a selector of namespaces that AzureCluster can + // use this Identity from. This is a standard Kubernetes LabelSelector, + // a label query over a set of resources. The result of matchLabels and + // matchExpressions are ANDed. + // + // A nil or empty selector indicates that AzureCluster cannot use this + // AzureClusterIdentity from any namespace. + // +optional + Selector *metav1.LabelSelector `json:"selector"` +} + // AzureClusterIdentitySpec defines the parameters that are used to create an AzureIdentity type AzureClusterIdentitySpec struct { // UserAssignedMSI or Service Principal @@ -36,14 +55,15 @@ type AzureClusterIdentitySpec struct { ClientSecret corev1.SecretReference `json:"clientSecret,omitempty"` // Service principal primary tenant id. TenantID string `json:"tenantID"` - // AllowedNamespaces is an array of namespaces that AzureClusters can - // use this Identity from. + // AllowedNamespaces is used to identify the namespaces the clusters are allowed to use the identity from. + // Namespaces can be selected either using an array of namespaces or with label selector. + // An empty allowedNamespaces object indicates that AzureClusters can use this identity from any namespace. + // If this object is nil, no namespaces will be allowed (default behaviour, if this field is not provided) + // A namespace should be either in the NamespaceList or match with Selector to use the identity. // - // An empty list (default) indicates that AzureClusters can use this - // Identity from any namespace. This field is intentionally not a - // pointer because the nil behavior (no namespaces) is undesirable here. // +optional - AllowedNamespaces []string `json:"allowedNamespaces"` + // +nullable + AllowedNamespaces *AllowedNamespaces `json:"allowedNamespaces"` } // AzureClusterIdentityStatus defines the observed state of AzureClusterIdentity @@ -86,20 +106,6 @@ func (c *AzureClusterIdentity) SetConditions(conditions clusterv1.Conditions) { c.Status.Conditions = conditions } -// ClusterNamespaceAllowed indicates if the cluster namespace is allowed -func (c *AzureClusterIdentity) ClusterNamespaceAllowed(namespace string) bool { - if len(c.Spec.AllowedNamespaces) == 0 { - return true - } - for _, v := range c.Spec.AllowedNamespaces { - if v == namespace { - return true - } - } - - return false -} - func init() { SchemeBuilder.Register(&AzureClusterIdentity{}, &AzureClusterIdentityList{}) } diff --git a/api/v1alpha4/azureclusteridentity_types_test.go b/api/v1alpha4/azureclusteridentity_types_test.go deleted file mode 100644 index af7dbf807db..00000000000 --- a/api/v1alpha4/azureclusteridentity_types_test.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright 2021 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha4 - -import ( - "testing" - - . "github.com/onsi/gomega" -) - -func TestAllowedNamespaces(t *testing.T) { - g := NewWithT(t) - - tests := []struct { - name string - identity *AzureClusterIdentity - clusterNamespace string - expected bool - }{ - { - name: "allow any cluster namespace when empty", - identity: &AzureClusterIdentity{ - Spec: AzureClusterIdentitySpec{ - AllowedNamespaces: []string{}, - }, - }, - clusterNamespace: "default", - expected: true, - }, - { - name: "allow cluster with namespace in list", - identity: &AzureClusterIdentity{ - Spec: AzureClusterIdentitySpec{ - AllowedNamespaces: []string{"namespace24", "namespace32"}, - }, - }, - clusterNamespace: "namespace24", - expected: true, - }, - { - name: "don't allow cluster with namespace not in list", - identity: &AzureClusterIdentity{ - Spec: AzureClusterIdentitySpec{ - AllowedNamespaces: []string{"namespace24", "namespace32"}, - }, - }, - clusterNamespace: "namespace8", - expected: false, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := tc.identity.ClusterNamespaceAllowed(tc.clusterNamespace) - g.Expect(actual).To(Equal(tc.expected)) - }) - } -} diff --git a/api/v1alpha4/zz_generated.deepcopy.go b/api/v1alpha4/zz_generated.deepcopy.go index c5a96c5b45d..4bb1c4dc7e6 100644 --- a/api/v1alpha4/zz_generated.deepcopy.go +++ b/api/v1alpha4/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ package v1alpha4 import ( "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" apiv1alpha4 "sigs.k8s.io/cluster-api/api/v1alpha4" "sigs.k8s.io/cluster-api/errors" @@ -42,6 +43,31 @@ func (in *AddressRecord) DeepCopy() *AddressRecord { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AllowedNamespaces) DeepCopyInto(out *AllowedNamespaces) { + *out = *in + if in.NamespaceList != nil { + in, out := &in.NamespaceList, &out.NamespaceList + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedNamespaces. +func (in *AllowedNamespaces) DeepCopy() *AllowedNamespaces { + if in == nil { + return nil + } + out := new(AllowedNamespaces) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AzureCluster) DeepCopyInto(out *AzureCluster) { *out = *in @@ -134,8 +160,8 @@ func (in *AzureClusterIdentitySpec) DeepCopyInto(out *AzureClusterIdentitySpec) out.ClientSecret = in.ClientSecret if in.AllowedNamespaces != nil { in, out := &in.AllowedNamespaces, &out.AllowedNamespaces - *out = make([]string, len(*in)) - copy(*out, *in) + *out = new(AllowedNamespaces) + (*in).DeepCopyInto(*out) } } diff --git a/azure/scope/identity.go b/azure/scope/identity.go index 3948fd15997..07251c7603b 100644 --- a/azure/scope/identity.go +++ b/azure/scope/identity.go @@ -19,6 +19,7 @@ package scope import ( "context" "fmt" + "reflect" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" @@ -31,6 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" aadpodv1 "github.com/Azure/aad-pod-identity/pkg/apis/aadpodidentity/v1" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -161,3 +163,45 @@ func getAzureIdentityType(identity *infrav1.AzureClusterIdentity) (aadpodv1.Iden return 0, errors.New("AzureIdentity does not have a vaild type") } + +// IsClusterNamespaceAllowed indicates if the cluster namespace is allowed +func IsClusterNamespaceAllowed(ctx context.Context, k8sClient client.Client, allowedNamespaces *infrav1.AllowedNamespaces, namespace string) bool { + if allowedNamespaces == nil { + return false + } + + // empty value matches with all namespaces + if reflect.DeepEqual(*allowedNamespaces, infrav1.AllowedNamespaces{}) { + return true + } + + for _, v := range allowedNamespaces.NamespaceList { + if v == namespace { + return true + } + } + + // Check if clusterNamespace is in the namespaces selected by the identity's allowedNamespaces selector. + namespaces := &corev1.NamespaceList{} + selector, err := metav1.LabelSelectorAsSelector(allowedNamespaces.Selector) + if err != nil { + return false + } + + // If a Selector has a nil or empty selector, it should match nothing. + if selector.Empty() { + return false + } + + if err := k8sClient.List(ctx, namespaces, client.MatchingLabelsSelector{Selector: selector}); err != nil { + return false + } + + for _, n := range namespaces.Items { + if n.Name == namespace { + return true + } + } + + return false +} diff --git a/azure/scope/identity_test.go b/azure/scope/identity_test.go new file mode 100644 index 00000000000..01eb89ad34e --- /dev/null +++ b/azure/scope/identity_test.go @@ -0,0 +1,136 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package scope + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/runtime" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestAllowedNamespaces(t *testing.T) { + g := NewWithT(t) + scheme := runtime.NewScheme() + _ = infrav1.AddToScheme(scheme) + _ = corev1.AddToScheme(scheme) + + tests := []struct { + name string + identity *infrav1.AzureClusterIdentity + clusterNamespace string + expected bool + }{ + { + name: "allow any cluster namespace when empty", + identity: &infrav1.AzureClusterIdentity{ + Spec: infrav1.AzureClusterIdentitySpec{ + AllowedNamespaces: &infrav1.AllowedNamespaces{}, + }, + }, + clusterNamespace: "default", + expected: true, + }, + { + name: "no namespaces allowed when list is empty", + identity: &infrav1.AzureClusterIdentity{ + Spec: infrav1.AzureClusterIdentitySpec{ + AllowedNamespaces: &infrav1.AllowedNamespaces{ + NamespaceList: []string{}, + }, + }, + }, + clusterNamespace: "default", + expected: false, + }, + { + name: "allow cluster with namespace in list", + identity: &infrav1.AzureClusterIdentity{ + Spec: infrav1.AzureClusterIdentitySpec{ + AllowedNamespaces: &infrav1.AllowedNamespaces{ + NamespaceList: []string{"namespace24", "namespace32"}, + }, + }, + }, + clusterNamespace: "namespace24", + expected: true, + }, + { + name: "don't allow cluster with namespace not in list", + identity: &infrav1.AzureClusterIdentity{ + Spec: infrav1.AzureClusterIdentitySpec{ + AllowedNamespaces: &infrav1.AllowedNamespaces{ + NamespaceList: []string{"namespace24", "namespace32"}, + }, + }, + }, + clusterNamespace: "namespace8", + expected: false, + }, + { + name: "allow cluster when namespace has selector with matching label", + identity: &infrav1.AzureClusterIdentity{ + Spec: infrav1.AzureClusterIdentitySpec{ + AllowedNamespaces: &infrav1.AllowedNamespaces{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"c": "d"}, + }, + }, + }, + }, + clusterNamespace: "namespace8", + expected: true, + }, + { + name: "don't allow cluster when namespace has selector with different label", + identity: &infrav1.AzureClusterIdentity{ + Spec: infrav1.AzureClusterIdentitySpec{ + AllowedNamespaces: &infrav1.AllowedNamespaces{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"x": "y"}, + }, + }, + }, + }, + clusterNamespace: "namespace8", + expected: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + + fakeNamespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "namespace8", + Labels: map[string]string{"c": "d"}, + }, + } + initObjects := []runtime.Object{tc.identity, fakeNamespace} + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjects...).Build() + + actual := IsClusterNamespaceAllowed(context.TODO(), fakeClient, tc.identity.Spec.AllowedNamespaces, tc.clusterNamespace) + g.Expect(actual).To(Equal(tc.expected)) + }) + } +} diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusteridentities.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusteridentities.yaml index 1f5a33e4f9f..e6a3d661bd3 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusteridentities.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_azureclusteridentities.yaml @@ -124,10 +124,46 @@ spec: description: AzureClusterIdentitySpec defines the parameters that are used to create an AzureIdentity properties: allowedNamespaces: - description: "AllowedNamespaces is an array of namespaces that AzureClusters can use this Identity from. \n An empty list (default) indicates that AzureClusters can use this Identity from any namespace. This field is intentionally not a pointer because the nil behavior (no namespaces) is undesirable here." - items: - type: string - type: array + description: AllowedNamespaces is used to identify the namespaces the clusters are allowed to use the identity from. Namespaces can be selected either using an array of namespaces or with label selector. An empty allowedNamespaces object indicates that AzureClusters can use this identity from any namespace. If this object is nil, no namespaces will be allowed (default behaviour, if this field is not provided) A namespace should be either in the NamespaceList or match with Selector to use the identity. + nullable: true + properties: + list: + description: A nil or empty list indicates that AzureCluster cannot use the identity from any namespace. + items: + type: string + nullable: true + type: array + selector: + description: "Selector is a selector of namespaces that AzureCluster can use this Identity from. This is a standard Kubernetes LabelSelector, a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. \n A nil or empty selector indicates that AzureCluster cannot use this AzureClusterIdentity from any namespace." + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + type: object clientID: description: Both User Assigned MSI and SP can use this field. type: string diff --git a/controllers/azurecluster_controller.go b/controllers/azurecluster_controller.go index f4e8f2f88d9..6a3759ec9fd 100644 --- a/controllers/azurecluster_controller.go +++ b/controllers/azurecluster_controller.go @@ -199,7 +199,7 @@ func (r *AzureClusterReconciler) reconcileNormal(ctx context.Context, clusterSco if err != nil { return reconcile.Result{}, err } - if !identity.ClusterNamespaceAllowed(azureCluster.Namespace) { + if !scope.IsClusterNamespaceAllowed(ctx, r.Client, identity.Spec.AllowedNamespaces, azureCluster.Namespace) { conditions.MarkFalse(azureCluster, infrav1.NetworkInfrastructureReadyCondition, infrav1.NamespaceNotAllowedByIdentity, clusterv1.ConditionSeverityError, "") return reconcile.Result{}, errors.New("AzureClusterIdentity list of allowed namespaces doesn't include current cluster namespace") } diff --git a/docs/book/src/topics/multitenancy.md b/docs/book/src/topics/multitenancy.md index 4e80def9b4a..a7f13dd0da4 100644 --- a/docs/book/src/topics/multitenancy.md +++ b/docs/book/src/topics/multitenancy.md @@ -20,7 +20,8 @@ spec: clientID: clientSecret: {"name":"","namespace":"default"} allowedNamespaces: - - + list: + - ``` The password will need to be added in a secret similar to the following example: @@ -49,8 +50,11 @@ data: ``` ## allowedNamespaces -AllowedNamespaces is an array of namespaces that AzureClusters can use this Identity from. CAPZ will not support AzureClusters in namespaces outside this list. -An empty list (default) indicates that AzureCluster can use this AzureClusterIdentity from any namespace. +AllowedNamespaces is used to identify the namespaces the clusters are allowed to use the identity from. Namespaces can be selected either using an array of namespaces or with label selector. +An empty allowedNamespaces object indicates that AzureClusters can use this identity from any namespace. +If this object is nil, no namespaces will be allowed (default behaviour, if this field is not provided) +A namespace should be either in the NamespaceList or match with Selector to use the identity. +Please note NamespaceList will take precedence over Selector if both are set. ## IdentityRef in AzureCluster diff --git a/templates/cluster-template-multi-tenancy.yaml b/templates/cluster-template-multi-tenancy.yaml index 4305a90306a..bb0af5faf03 100644 --- a/templates/cluster-template-multi-tenancy.yaml +++ b/templates/cluster-template-multi-tenancy.yaml @@ -201,6 +201,7 @@ metadata: name: ${CLUSTER_IDENTITY_NAME} namespace: default spec: + allowedNamespaces: {} clientID: ${AZURE_CLUSTER_IDENTITY_CLIENT_ID} clientSecret: name: ${AZURE_CLUSTER_IDENTITY_SECRET_NAME} diff --git a/templates/flavors/multi-tenancy/azure-cluster-identity.yaml b/templates/flavors/multi-tenancy/azure-cluster-identity.yaml index a418c489587..337bdc8360a 100644 --- a/templates/flavors/multi-tenancy/azure-cluster-identity.yaml +++ b/templates/flavors/multi-tenancy/azure-cluster-identity.yaml @@ -5,6 +5,7 @@ metadata: name: "${CLUSTER_IDENTITY_NAME}" spec: type: ServicePrincipal + allowedNamespaces: {} tenantID: "${AZURE_TENANT_ID}" clientID: "${AZURE_CLUSTER_IDENTITY_CLIENT_ID}" clientSecret: {"name":"${AZURE_CLUSTER_IDENTITY_SECRET_NAME}","namespace":"${AZURE_CLUSTER_IDENTITY_SECRET_NAMESPACE}"} diff --git a/templates/test/ci/cluster-template-prow-multi-tenancy.yaml b/templates/test/ci/cluster-template-prow-multi-tenancy.yaml index 1ae2c0135f0..a448ff2f338 100644 --- a/templates/test/ci/cluster-template-prow-multi-tenancy.yaml +++ b/templates/test/ci/cluster-template-prow-multi-tenancy.yaml @@ -206,6 +206,7 @@ metadata: name: ${CLUSTER_IDENTITY_NAME} namespace: default spec: + allowedNamespaces: {} clientID: ${AZURE_CLUSTER_IDENTITY_CLIENT_ID} clientSecret: name: ${AZURE_CLUSTER_IDENTITY_SECRET_NAME}