diff --git a/cloud/util/suite_test.go b/cloud/util/suite_test.go new file mode 100644 index 000000000..b11da6071 --- /dev/null +++ b/cloud/util/suite_test.go @@ -0,0 +1,39 @@ +/* + Copyright (c) 2021, 2022 Oracle and/or its affiliates. + + 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 + + https://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 util + +import ( + "os" + "testing" + + infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" +) + +func TestMain(m *testing.M) { + code := 0 + defer func() { os.Exit(code) }() + setup() + code = m.Run() +} + +func setup() { + utilruntime.Must(infrastructurev1beta1.AddToScheme(scheme.Scheme)) + utilruntime.Must(clusterv1.AddToScheme(scheme.Scheme)) +} diff --git a/cloud/util/util.go b/cloud/util/util.go index 93e7259ae..abe53983b 100644 --- a/cloud/util/util.go +++ b/cloud/util/util.go @@ -142,7 +142,7 @@ func InitClientsAndRegion(ctx context.Context, client client.Client, defaultRegi // 2) If region is set in the cluster identity, that takes the next priority // 3) Last priority is for region set at the Pod initialization time OCI identity clusterRegion := defaultRegion - + identityRef := clusterAccessor.GetIdentityRef() // If Cluster identity is set, OCI Clients should be created using the identity if identityRef != nil { @@ -163,7 +163,7 @@ func InitClientsAndRegion(ctx context.Context, client client.Client, defaultRegi clusterRegion = clusterAccessor.GetRegion() } if len(clusterRegion) <= 0 { - return nil, "", scope.OCIClients{}, errors.New("RegionIdentifier could not be identified") + return nil, "", scope.OCIClients{}, errors.New("OCI Region could not be identified for the cluster") } clients, err := clientProvider.GetOrBuildClient(clusterRegion) if err != nil { diff --git a/cloud/util/util_test.go b/cloud/util/util_test.go new file mode 100644 index 000000000..4c71f375b --- /dev/null +++ b/cloud/util/util_test.go @@ -0,0 +1,300 @@ +/* +Copyright (c) 2021, 2022 Oracle and/or its affiliates. + +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 + + https://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 util + +import ( + "context" + "reflect" + "testing" + + . "github.com/onsi/gomega" + infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" + "github.com/oracle/cluster-api-provider-oci/cloud/config" + "github.com/oracle/cluster-api-provider-oci/cloud/scope" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestGetClusterIdentityFromRef(t *testing.T) { + testCases := []struct { + name string + namespace string + ref *corev1.ObjectReference + objects []client.Object + errorExpected bool + expectedSpec infrastructurev1beta1.OCIClusterIdentitySpec + }{ + { + name: "simple", + namespace: "default", + ref: &corev1.ObjectReference{ + Kind: "OCIClusterIdentity", + Namespace: "default", + Name: "test-identity", + APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", + }, + objects: []client.Object{&infrastructurev1beta1.OCIClusterIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-identity", + Namespace: "default", + }, + Spec: infrastructurev1beta1.OCIClusterIdentitySpec{ + Type: infrastructurev1beta1.UserPrincipal, + PrincipalSecret: corev1.SecretReference{ + Name: "test", + Namespace: "test", + }, + }, + }}, + expectedSpec: infrastructurev1beta1.OCIClusterIdentitySpec{ + Type: infrastructurev1beta1.UserPrincipal, + PrincipalSecret: corev1.SecretReference{ + Name: "test", + Namespace: "test", + }, + }, + }, + { + name: "error - not found", + namespace: "default", + ref: &corev1.ObjectReference{ + Kind: "OCIClusterIdentity", + Namespace: "default", + Name: "test-identity", + APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", + }, + objects: []client.Object{}, + errorExpected: true, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + client := fake.NewClientBuilder().WithObjects(tt.objects...).Build() + result, err := GetClusterIdentityFromRef(context.Background(), client, tt.namespace, tt.ref) + if tt.errorExpected { + g.Expect(err).To(Not(BeNil())) + } else { + g.Expect(err).To(BeNil()) + if !reflect.DeepEqual(tt.expectedSpec, result.Spec) { + t.Errorf("Test (%s) \n Expected %v, \n Actual %v", tt.name, tt.expectedSpec, result.Spec) + } + } + }) + } +} + +func TestGetOrBuildClientFromIdentity(t *testing.T) { + testCases := []struct { + name string + namespace string + clusterIdentity *infrastructurev1beta1.OCIClusterIdentity + objects []client.Object + errorExpected bool + defaultRegion string + }{ + { + name: "error - secret not found", + namespace: "default", + clusterIdentity: &infrastructurev1beta1.OCIClusterIdentity{ + Spec: infrastructurev1beta1.OCIClusterIdentitySpec{ + Type: infrastructurev1beta1.UserPrincipal, + PrincipalSecret: corev1.SecretReference{ + Name: "test", + Namespace: "test", + }, + }, + }, + objects: []client.Object{}, + errorExpected: true, + }, + { + name: "error - invalid principal type", + namespace: "default", + clusterIdentity: &infrastructurev1beta1.OCIClusterIdentity{ + Spec: infrastructurev1beta1.OCIClusterIdentitySpec{ + Type: "invalid", + }, + }, + objects: []client.Object{}, + errorExpected: true, + }, + { + name: "secret found", + namespace: "default", + clusterIdentity: &infrastructurev1beta1.OCIClusterIdentity{ + Spec: infrastructurev1beta1.OCIClusterIdentitySpec{ + Type: infrastructurev1beta1.UserPrincipal, + PrincipalSecret: corev1.SecretReference{ + Name: "test", + Namespace: "test", + }, + }, + }, + objects: []client.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Data: map[string][]byte{config.Tenancy: []byte("tenancy"), config.User: []byte("user"), + config.Key: []byte("key"), config.Fingerprint: []byte("fingerprint"), config.Region: []byte("region")}, + }}, + errorExpected: false, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + client := fake.NewClientBuilder().WithObjects(tt.objects...).Build() + _, err := GetOrBuildClientFromIdentity(context.Background(), client, tt.clusterIdentity, tt.defaultRegion) + if tt.errorExpected { + g.Expect(err).To(Not(BeNil())) + } else { + g.Expect(err).To(BeNil()) + } + }) + } +} + +func TestIsClusterNamespaceAllowed(t *testing.T) { + testCases := []struct { + name string + namespace string + allowedNamespaces *infrastructurev1beta1.AllowedNamespaces + objects []client.Object + expected bool + }{ + { + name: "nil allowednamespace, not allowed", + namespace: "default", + objects: []client.Object{}, + expected: false, + }, + { + name: "empty allowednamespace, allowed", + namespace: "default", + allowedNamespaces: &infrastructurev1beta1.AllowedNamespaces{}, + objects: []client.Object{}, + expected: true, + }, + { + name: "not allowed", + namespace: "test", + allowedNamespaces: &infrastructurev1beta1.AllowedNamespaces{ + NamespaceList: []string{"test123"}, + }, + objects: []client.Object{}, + expected: false, + }, + { + name: "allowed", + namespace: "test", + allowedNamespaces: &infrastructurev1beta1.AllowedNamespaces{ + NamespaceList: []string{"test"}, + }, + objects: []client.Object{}, + expected: true, + }, + { + name: "empty label selector", + namespace: "test", + allowedNamespaces: &infrastructurev1beta1.AllowedNamespaces{ + Selector: &metav1.LabelSelector{}, + }, + objects: []client.Object{}, + expected: false, + }, + { + name: "allowed label selector", + namespace: "test", + allowedNamespaces: &infrastructurev1beta1.AllowedNamespaces{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"key": "value"}, + }, + }, + objects: []client.Object{&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + Labels: map[string]string{"key": "value"}, + }, + }}, + expected: true, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + client := fake.NewClientBuilder().WithObjects(tt.objects...).Build() + result := IsClusterNamespaceAllowed(context.Background(), client, tt.allowedNamespaces, tt.namespace) + g.Expect(result).To(BeEquivalentTo(tt.expected)) + }) + } +} + +func TestCreateClientProviderFromClusterIdentity(t *testing.T) { + testCases := []struct { + name string + namespace string + objects []client.Object + clusterAccessor scope.OCIClusterAccessor + ref *corev1.ObjectReference + errorExpected bool + defaultRegion string + }{ + { + name: "error - secret not found", + namespace: "default", + clusterAccessor: scope.OCISelfManagedCluster{ + OCICluster: &infrastructurev1beta1.OCICluster{}, + }, + ref: &corev1.ObjectReference{ + Kind: "OCIClusterIdentity", + Namespace: "default", + Name: "test-identity", + APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", + }, + objects: []client.Object{&infrastructurev1beta1.OCIClusterIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-identity", + Namespace: "default", + }, + Spec: infrastructurev1beta1.OCIClusterIdentitySpec{ + Type: infrastructurev1beta1.UserPrincipal, + PrincipalSecret: corev1.SecretReference{ + Name: "test", + Namespace: "test", + }, + }, + }}, + errorExpected: true, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + client := fake.NewClientBuilder().WithObjects(tt.objects...).Build() + _, err := CreateClientProviderFromClusterIdentity(context.Background(), client, tt.namespace, tt.defaultRegion, tt.clusterAccessor, tt.ref) + if tt.errorExpected { + g.Expect(err).To(Not(BeNil())) + } else { + g.Expect(err).To(BeNil()) + } + }) + } +} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 7a8ebfdb3..302604f10 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -27,6 +27,7 @@ - [Provision a PVC on the Block Volume Service](./gs/pvc-bv.md) - [Provision a PVC on the File Storage Service](./gs/pvc-fss.md) - [Customize worker nodes](./gs/customize-worker-node.md) + - [Multi Tenancy](./gs/multi-tenancy.md) - [Networking Guide](./networking/networking.md) - [Default Network Infrastructure](./networking/infrastructure.md) - [Using Calico](./networking/calico.md) diff --git a/docs/src/gs/multi-tenancy.md b/docs/src/gs/multi-tenancy.md new file mode 100644 index 000000000..301beb9f8 --- /dev/null +++ b/docs/src/gs/multi-tenancy.md @@ -0,0 +1,70 @@ +# Multi-tenancy + +CAPOCI supports multi-tenancy wherein different OCI user principals can be used to reconcile +different OCI clusters. This is achieved by associating a cluster with a Cluster Identity and +associating the identity with a user principal. Currently only OCI user principal is supported +for Cluster Identity. + +# Steps + +## Step 1 - Create a secret with user principal in the management cluster + +Please read the [doc][iam-user] to know more about the parameters below. + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: user-credentials + namespace: default +type: Opaque +data: + tenancy: + user: + key: + fingerprint: + passphrase: + region: +``` + +## Step 2 - Edit the cluster template to add a Cluster Identity section and point the OCICluster to the Cluster Identity + +The Cluster Identity should have a reference to the secret created above. + +```yaml +--- +kind: OCIClusterIdentity +metadata: + name: cluster-identity + namespace: default +spec: + type: UserPrincipal + principalSecret: + name: user-credentials + namespace: default + allowedNamespaces: {} +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OCICluster +metadata: + labels: + cluster.x-k8s.io/cluster-name: "${CLUSTER_NAME}" + name: "${CLUSTER_NAME}" +spec: + compartmentId: "${OCI_COMPARTMENT_ID}" + identityRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OCIClusterIdentity + name: cluster-identity + namespace: default +``` + +# allowedNamespaces + +AllowedNamespaces can be used to control which namespaces the OCIClusters 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 OCIClusters can use this identity from any namespace. +If this object is nil, no namespaces will be allowed, which is the default behavior in the field is not specified. +Please note NamespaceList will take precedence over Selector if both are set. + +[iam-user]: https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm#Required_Keys_and_OCIDs \ No newline at end of file diff --git a/test/e2e/cluster_test.go b/test/e2e/cluster_test.go index f7136fe33..1b442858b 100644 --- a/test/e2e/cluster_test.go +++ b/test/e2e/cluster_test.go @@ -556,7 +556,7 @@ var _ = Describe("Workload cluster creation", func() { }) }) - It("Cluster identity - with 1 control-plane nodes and 1 worker nodes", func() { + It("Cluster Identity - with 1 control-plane nodes and 1 worker nodes", func() { clusterName = getClusterName(clusterNamePrefix, "cluster-identity") clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{ ClusterProxy: bootstrapClusterProxy,