diff --git a/Makefile b/Makefile index bb1b9e0b..a96e850b 100644 --- a/Makefile +++ b/Makefile @@ -253,6 +253,8 @@ generate-e2e-templates: $(KUSTOMIZE) $(KUSTOMIZE) build $(OCI_TEMPLATES)/v1beta1/cluster-template-externally-managed-vcn --load-restrictor LoadRestrictionsNone > $(OCI_TEMPLATES)/v1beta1/cluster-template-externally-managed-vcn.yaml $(KUSTOMIZE) build $(OCI_TEMPLATES)/v1beta1/cluster-template-machine-pool --load-restrictor LoadRestrictionsNone > $(OCI_TEMPLATES)/v1beta1/cluster-template-machine-pool.yaml $(KUSTOMIZE) build $(OCI_TEMPLATES)/v1beta1/cluster-template-managed --load-restrictor LoadRestrictionsNone > $(OCI_TEMPLATES)/v1beta1/cluster-template-managed.yaml + $(KUSTOMIZE) build $(OCI_TEMPLATES)/v1beta1/cluster-template-managed-cluster-identity --load-restrictor LoadRestrictionsNone > $(OCI_TEMPLATES)/v1beta1/cluster-template-managed-cluster-identity.yaml + $(KUSTOMIZE) build $(OCI_TEMPLATES)/v1beta1/cluster-template-cluster-identity --load-restrictor LoadRestrictionsNone > $(OCI_TEMPLATES)/v1beta1/cluster-template-cluster-identity.yaml .PHONY: test-e2e-run test-e2e-run: generate-e2e-templates $(GINKGO) $(ENVSUBST) ## Run e2e tests diff --git a/api/v1beta1/conditions_consts.go b/api/v1beta1/conditions_consts.go index daf53b71..88b1616c 100644 --- a/api/v1beta1/conditions_consts.go +++ b/api/v1beta1/conditions_consts.go @@ -99,4 +99,6 @@ const ( ApiServerLoadBalancerEventReady = "APIServerLoadBalancerReady" // FailureDomainEventReady used after reconciliation has completed successfully FailureDomainEventReady = "FailureDomainsReady" + // NamespaceNotAllowedByIdentity used to indicate cluster in a namespace not allowed by identity. + NamespaceNotAllowedByIdentity = "NamespaceNotAllowedByIdentity" ) diff --git a/api/v1beta1/ocicluster_types.go b/api/v1beta1/ocicluster_types.go index 0359ab3b..a717a1dd 100644 --- a/api/v1beta1/ocicluster_types.go +++ b/api/v1beta1/ocicluster_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1beta1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ) @@ -39,6 +40,10 @@ type OCIClusterSpec struct { // +optional OCIResourceIdentifier string `json:"ociResourceIdentifier,omitempty"` + // IdentityRef is a reference to an identity(principal) to be used when reconciling this cluster + // +optional + IdentityRef *corev1.ObjectReference `json:"identityRef,omitempty"` + // NetworkSpec encapsulates all things related to OCI network. // +optional NetworkSpec NetworkSpec `json:"networkSpec,omitempty"` diff --git a/api/v1beta1/ociclusteridentity_types.go b/api/v1beta1/ociclusteridentity_types.go new file mode 100644 index 00000000..97cbc0c9 --- /dev/null +++ b/api/v1beta1/ociclusteridentity_types.go @@ -0,0 +1,101 @@ +/* + Copyright (c) 2022, 2023 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 v1beta1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" +) + +type PrincipalType string + +const ( + // UserPrincipal represents a user principal. + UserPrincipal PrincipalType = "UserPrincipal" +) + +// OCIClusterIdentitySpec defines the parameters that are used to create an OCIClusterIdentity. +type OCIClusterIdentitySpec struct { + // Type is the type of OCI Principal used. + // UserPrincipal is the only supported value + Type PrincipalType `json:"type"` + + // PrincipalSecret is a secret reference which contains the authentication credentials for the principal. + // +optional + PrincipalSecret corev1.SecretReference `json:"principalSecret,omitempty"` + + // 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 OCIClusters 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. + // + // +optional + // +nullable + AllowedNamespaces *AllowedNamespaces `json:"allowedNamespaces"` +} + +// AllowedNamespaces defines the namespaces the clusters are allowed to use the identity from +type AllowedNamespaces struct { + // A nil or empty list indicates that OCICluster cannot use the identity from any namespace. + // NamespaceList takes precedence over the Selector. + // +optional + // +nullable + NamespaceList []string `json:"list"` + + // Selector is a selector of namespaces that OCICluster 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 OCICluster cannot use this + // OCIClusterIdentity from any namespace. + // +optional + Selector *metav1.LabelSelector `json:"selector"` +} + +// OCIClusterIdentityStatus defines the observed state of OCIClusterIdentity. +type OCIClusterIdentityStatus struct { + // Conditions defines current service state of the OCIClusterIdentity. + // +optional + Conditions clusterv1.Conditions `json:"conditions,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// OCIClusterIdentity is the Schema for the OCI Cluster Identity API +type OCIClusterIdentity struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec OCIClusterIdentitySpec `json:"spec,omitempty"` + Status OCIClusterIdentityStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// OCIClusterIdentityList contains a list of OCIClusterIdentity. +type OCIClusterIdentityList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OCIClusterIdentity `json:"items"` +} + +func init() { + SchemeBuilder.Register(&OCIClusterIdentity{}, &OCIClusterIdentityList{}) +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 1d9ef125..655f4e0d 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -22,11 +22,38 @@ limitations under the License. package v1beta1 import ( + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" apiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/errors" ) +// 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 *AmdMilanBmPlatformConfig) DeepCopyInto(out *AmdMilanBmPlatformConfig) { *out = *in @@ -829,6 +856,108 @@ func (in *OCICluster) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OCIClusterIdentity) DeepCopyInto(out *OCIClusterIdentity) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIClusterIdentity. +func (in *OCIClusterIdentity) DeepCopy() *OCIClusterIdentity { + if in == nil { + return nil + } + out := new(OCIClusterIdentity) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OCIClusterIdentity) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OCIClusterIdentityList) DeepCopyInto(out *OCIClusterIdentityList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OCIClusterIdentity, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIClusterIdentityList. +func (in *OCIClusterIdentityList) DeepCopy() *OCIClusterIdentityList { + if in == nil { + return nil + } + out := new(OCIClusterIdentityList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OCIClusterIdentityList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OCIClusterIdentitySpec) DeepCopyInto(out *OCIClusterIdentitySpec) { + *out = *in + out.PrincipalSecret = in.PrincipalSecret + if in.AllowedNamespaces != nil { + in, out := &in.AllowedNamespaces, &out.AllowedNamespaces + *out = new(AllowedNamespaces) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIClusterIdentitySpec. +func (in *OCIClusterIdentitySpec) DeepCopy() *OCIClusterIdentitySpec { + if in == nil { + return nil + } + out := new(OCIClusterIdentitySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OCIClusterIdentityStatus) DeepCopyInto(out *OCIClusterIdentityStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(apiv1beta1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIClusterIdentityStatus. +func (in *OCIClusterIdentityStatus) DeepCopy() *OCIClusterIdentityStatus { + if in == nil { + return nil + } + out := new(OCIClusterIdentityStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OCIClusterList) DeepCopyInto(out *OCIClusterList) { *out = *in @@ -864,6 +993,11 @@ func (in *OCIClusterList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OCIClusterSpec) DeepCopyInto(out *OCIClusterSpec) { *out = *in + if in.IdentityRef != nil { + in, out := &in.IdentityRef, &out.IdentityRef + *out = new(v1.ObjectReference) + **out = **in + } in.NetworkSpec.DeepCopyInto(&out.NetworkSpec) if in.FreeformTags != nil { in, out := &in.FreeformTags, &out.FreeformTags diff --git a/cloud/scope/clients.go b/cloud/scope/clients.go index 89d2f738..67bccb12 100644 --- a/cloud/scope/clients.go +++ b/cloud/scope/clients.go @@ -100,6 +100,11 @@ func (c *ClientProvider) GetOrBuildClient(region string) (OCIClients, error) { return regionalClient, nil } +// GetRegion returns the region from the authentication config provider +func (c *ClientProvider) GetRegion() (string, error) { + return c.ociAuthConfigProvider.Region() +} + func createClients(region string, oCIAuthConfigProvider common.ConfigurationProvider, logger *logr.Logger) (OCIClients, error) { vcnClient, err := createVncClient(region, oCIAuthConfigProvider, logger) if err != nil { diff --git a/cloud/scope/cluster_accessor.go b/cloud/scope/cluster_accessor.go index f531cd72..d1f0f44c 100644 --- a/cloud/scope/cluster_accessor.go +++ b/cloud/scope/cluster_accessor.go @@ -18,6 +18,7 @@ package scope import ( infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" + corev1 "k8s.io/api/core/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ) @@ -37,6 +38,10 @@ type OCIClusterAccessor interface { GetFreeformTags() map[string]string // GetName returns the name of the cluster. GetName() string + // GetNameSpace returns the namespace of the cluster. + GetNameSpace() string + // GetRegion returns the region of the cluster, if specified in the spec. + GetRegion() string // GetNetworkSpec returns the NetworkSpec of the cluster. GetNetworkSpec() *infrastructurev1beta1.NetworkSpec // SetControlPlaneEndpoint sets the control plane endpoint of the cluster. @@ -47,4 +52,8 @@ type OCIClusterAccessor interface { SetFailureDomain(id string, spec clusterv1.FailureDomainSpec) // SetAvailabilityDomains sets the availability domain. SetAvailabilityDomains(ads map[string]infrastructurev1beta1.OCIAvailabilityDomain) + // MarkConditionFalse marks the provided condition as false in the cluster object + MarkConditionFalse(t clusterv1.ConditionType, reason string, severity clusterv1.ConditionSeverity, messageFormat string, messageArgs ...interface{}) + // GetIdentityRef returns the Identity reference of the cluster + GetIdentityRef() *corev1.ObjectReference } diff --git a/cloud/scope/oci_managed_cluster.go b/cloud/scope/oci_managed_cluster.go index 8f9a44f5..97d11fcf 100644 --- a/cloud/scope/oci_managed_cluster.go +++ b/cloud/scope/oci_managed_cluster.go @@ -19,7 +19,9 @@ package scope import ( infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" infrav1exp "github.com/oracle/cluster-api-provider-oci/exp/api/v1beta1" + corev1 "k8s.io/api/core/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util/conditions" ) // OCIManagedCluster is the ClusterAccessor implementation for managed clusters(OKE) @@ -27,6 +29,23 @@ type OCIManagedCluster struct { OCIManagedCluster *infrav1exp.OCIManagedCluster } +func (c OCIManagedCluster) GetNameSpace() string { + return c.OCIManagedCluster.Namespace +} + +func (c OCIManagedCluster) GetRegion() string { + return c.OCIManagedCluster.Spec.Region +} + +func (c OCIManagedCluster) MarkConditionFalse(t clusterv1.ConditionType, reason string, severity clusterv1.ConditionSeverity, messageFormat string, messageArgs ...interface{}) { + conditions.MarkFalse(c.OCIManagedCluster, infrastructurev1beta1.ClusterReadyCondition, reason, severity, messageFormat, messageArgs...) + +} + +func (c OCIManagedCluster) GetIdentityRef() *corev1.ObjectReference { + return c.OCIManagedCluster.Spec.IdentityRef +} + func (c OCIManagedCluster) GetOCIResourceIdentifier() string { return c.OCIManagedCluster.Spec.OCIResourceIdentifier } diff --git a/cloud/scope/oci_selfmanaged_cluster.go b/cloud/scope/oci_selfmanaged_cluster.go index ac5ec47d..1a30a766 100644 --- a/cloud/scope/oci_selfmanaged_cluster.go +++ b/cloud/scope/oci_selfmanaged_cluster.go @@ -18,7 +18,9 @@ package scope import ( infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" + corev1 "k8s.io/api/core/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/cluster-api/util/conditions" ) // OCISelfManagedCluster is the ClusterAccessor implementation for self managed clusters @@ -26,6 +28,22 @@ type OCISelfManagedCluster struct { OCICluster *infrastructurev1beta1.OCICluster } +func (c OCISelfManagedCluster) GetNameSpace() string { + return c.OCICluster.Namespace +} + +func (c OCISelfManagedCluster) GetRegion() string { + return c.OCICluster.Spec.Region +} + +func (c OCISelfManagedCluster) GetIdentityRef() *corev1.ObjectReference { + return c.OCICluster.Spec.IdentityRef +} + +func (c OCISelfManagedCluster) MarkConditionFalse(t clusterv1.ConditionType, reason string, severity clusterv1.ConditionSeverity, messageFormat string, messageArgs ...interface{}) { + conditions.MarkFalse(c.OCICluster, infrastructurev1beta1.ClusterReadyCondition, reason, severity, messageFormat, messageArgs...) +} + func (c OCISelfManagedCluster) GetOCIResourceIdentifier() string { return c.OCICluster.Spec.OCIResourceIdentifier } diff --git a/cloud/util/suite_test.go b/cloud/util/suite_test.go new file mode 100644 index 00000000..b11da607 --- /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 new file mode 100644 index 00000000..abe53983 --- /dev/null +++ b/cloud/util/util.go @@ -0,0 +1,190 @@ +/* + 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" + "fmt" + "reflect" + + 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" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// GetClusterIdentityFromRef returns the OCIClusterIdentity referenced by the OCICluster. +func GetClusterIdentityFromRef(ctx context.Context, c client.Client, ociClusterNamespace string, ref *corev1.ObjectReference) (*infrastructurev1beta1.OCIClusterIdentity, error) { + identity := &infrastructurev1beta1.OCIClusterIdentity{} + if ref != nil { + namespace := ref.Namespace + if namespace == "" { + namespace = ociClusterNamespace + } + key := client.ObjectKey{Name: ref.Name, Namespace: namespace} + if err := c.Get(ctx, key, identity); err != nil { + return nil, err + } + return identity, nil + } + return nil, nil +} + +// GetOrBuildClientFromIdentity creates ClientProvider from OCIClusterIdentity object +func GetOrBuildClientFromIdentity(ctx context.Context, c client.Client, identity *infrastructurev1beta1.OCIClusterIdentity, defaultRegion string) (*scope.ClientProvider, error) { + if identity.Spec.Type == infrastructurev1beta1.UserPrincipal { + secretRef := identity.Spec.PrincipalSecret + key := types.NamespacedName{ + Namespace: secretRef.Namespace, + Name: secretRef.Name, + } + secret := &corev1.Secret{} + + if err := c.Get(ctx, key, secret); err != nil { + return nil, errors.Wrap(err, "Unable to fetch ClientSecret") + } + + tenancyId := string(secret.Data[config.Tenancy]) + userId := string(secret.Data[config.User]) + fingerPrint := string(secret.Data[config.Fingerprint]) + passphrase := string(secret.Data[config.Passphrase]) + privatekey := string(secret.Data[config.Key]) + region := string(secret.Data[config.Region]) + // set the default region if not provided in the secret + if region == "" { + region = defaultRegion + } + conf := common.NewRawConfigurationProvider( + tenancyId, + userId, + region, + fingerPrint, + privatekey, + common.String(passphrase)) + + clientProvider, err := scope.NewClientProvider(conf) + if err != nil { + return nil, err + } + return clientProvider, nil + } + return nil, errors.New(fmt.Sprintf("invalid oci principal format type: %s", identity.Spec.Type)) +} + +// IsClusterNamespaceAllowed indicates if the cluster namespace is allowed. +func IsClusterNamespaceAllowed(ctx context.Context, k8sClient client.Client, allowedNamespaces *infrastructurev1beta1.AllowedNamespaces, namespace string) bool { + if allowedNamespaces == nil { + return false + } + + // empty value matches with all namespaces + if reflect.DeepEqual(*allowedNamespaces, infrastructurev1beta1.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 +} + +// InitClientsAndRegion initializes the OCI Clients and Region based on various parameters +func InitClientsAndRegion(ctx context.Context, client client.Client, defaultRegion string, clusterAccessor scope.OCIClusterAccessor, defaultClientProvider *scope.ClientProvider) (*scope.ClientProvider, string, scope.OCIClients, error) { + var clientProvider *scope.ClientProvider + var err error + // Region is calculated as follows + // 1) If region is set in the cluster spec, that takes highest priority + // 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 { + clientProvider, err = CreateClientProviderFromClusterIdentity(ctx, client, clusterAccessor.GetNameSpace(), defaultRegion, clusterAccessor, identityRef) + if err != nil { + return nil, "", scope.OCIClients{}, err + } + region, err := clientProvider.GetRegion() + if err != nil { + return nil, "", scope.OCIClients{}, err + } + clusterRegion = region + } else { + clientProvider = defaultClientProvider + } + // Region set at cluster takes highest precedence + if len(clusterAccessor.GetRegion()) > 0 { + clusterRegion = clusterAccessor.GetRegion() + } + if len(clusterRegion) <= 0 { + return nil, "", scope.OCIClients{}, errors.New("OCI Region could not be identified for the cluster") + } + clients, err := clientProvider.GetOrBuildClient(clusterRegion) + if err != nil { + return nil, "", scope.OCIClients{}, err + } + return clientProvider, clusterRegion, clients, nil +} + +// CreateClientProviderFromClusterIdentity creates scope.ClientProvider from Cluster Identity +func CreateClientProviderFromClusterIdentity(ctx context.Context, client client.Client, namespace string, defaultRegion string, clusterAccessor scope.OCIClusterAccessor, identityRef *corev1.ObjectReference) (*scope.ClientProvider, error) { + identity, err := GetClusterIdentityFromRef(ctx, client, namespace, identityRef) + if err != nil { + return nil, err + } + if !IsClusterNamespaceAllowed(ctx, client, identity.Spec.AllowedNamespaces, namespace) { + clusterAccessor.MarkConditionFalse(infrastructurev1beta1.ClusterReadyCondition, infrastructurev1beta1.NamespaceNotAllowedByIdentity, clusterv1.ConditionSeverityError, "") + return nil, errors.Errorf("OCIClusterIdentity list of allowed namespaces doesn't include current cluster namespace %s", namespace) + } + clientProvider, err := GetOrBuildClientFromIdentity(ctx, client, identity, defaultRegion) + if err != nil { + return nil, err + } + return clientProvider, nil +} diff --git a/cloud/util/util_test.go b/cloud/util/util_test.go new file mode 100644 index 00000000..4c71f375 --- /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/config/crd/bases/infrastructure.cluster.x-k8s.io_ociclusteridentities.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ociclusteridentities.yaml new file mode 100644 index 00000000..b073342d --- /dev/null +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ociclusteridentities.yaml @@ -0,0 +1,184 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: ociclusteridentities.infrastructure.cluster.x-k8s.io +spec: + group: infrastructure.cluster.x-k8s.io + names: + kind: OCIClusterIdentity + listKind: OCIClusterIdentityList + plural: ociclusteridentities + singular: ociclusteridentity + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: OCIClusterIdentity is the Schema for the OCI Cluster Identity + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: OCIClusterIdentitySpec defines the parameters that are used + to create an OCIClusterIdentity. + properties: + allowedNamespaces: + 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 OCIClusters 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 OCICluster cannot + use the identity from any namespace. NamespaceList takes precedence + over the Selector. + items: + type: string + nullable: true + type: array + selector: + description: "Selector is a selector of namespaces that OCICluster + 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 OCICluster cannot use this OCIClusterIdentity 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 + x-kubernetes-map-type: atomic + type: object + principalSecret: + description: PrincipalSecret is a secret reference which contains + the authentication credentials for the principal. + properties: + name: + description: name is unique within a namespace to reference a + secret resource. + type: string + namespace: + description: namespace defines the space within which the secret + name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type is the type of OCI Principal used. UserPrincipal + is the only supported value + type: string + required: + - type + type: object + status: + description: OCIClusterIdentityStatus defines the observed state of OCIClusterIdentity. + properties: + conditions: + description: Conditions defines current service state of the OCIClusterIdentity. + items: + description: Condition defines an observation of a Cluster API resource + operational state. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. This should be when the underlying condition changed. + If that is not known, then using the time when the API field + changed is acceptable. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. This field may be empty. + type: string + reason: + description: The reason for the condition's last transition + in CamelCase. The specific API may choose whether or not this + field is considered a guaranteed API. This field may not be + empty. + type: string + severity: + description: Severity provides an explicit classification of + Reason code, so the users or machines can immediately understand + the current situation and act accordingly. The Severity field + MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ociclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ociclusters.yaml index 169aefc6..2e346aa1 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ociclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ociclusters.yaml @@ -68,6 +68,44 @@ spec: type: string description: Free-form tags for this resource. type: object + identityRef: + description: IdentityRef is a reference to an identity(principal) + to be used when reconciling this cluster + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic networkSpec: description: NetworkSpec encapsulates all things related to OCI network. properties: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ociclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ociclustertemplates.yaml index c1bbb2e1..d807e43f 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ociclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ociclustertemplates.yaml @@ -78,6 +78,45 @@ spec: type: string description: Free-form tags for this resource. type: object + identityRef: + description: IdentityRef is a reference to an identity(principal) + to be used when reconciling this cluster + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead + of an entire object, this string should contain a valid + JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container + within a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that + triggered the event) or if no container name is specified + "spec.containers[2]" (container with index 2 in this + pod). This syntax is chosen only to have some well-defined + way of referencing a part of an object. TODO: this design + is not final and this field is subject to change in + the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic networkSpec: description: NetworkSpec encapsulates all things related to OCI network. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ocimanagedclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ocimanagedclusters.yaml index 72d804b1..c419c9df 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ocimanagedclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ocimanagedclusters.yaml @@ -71,6 +71,43 @@ spec: type: string description: Free-form tags for this resource. type: object + identityRef: + description: IdentityRef is a reference to an identity(principal) + to be used when reconciling this cluster + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object networkSpec: description: NetworkSpec encapsulates all things related to OCI network. properties: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ocimanagedclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ocimanagedclustertemplates.yaml index 0f012b8d..d5f8d76a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ocimanagedclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ocimanagedclustertemplates.yaml @@ -83,6 +83,44 @@ spec: type: string description: Free-form tags for this resource. type: object + identityRef: + description: IdentityRef is a reference to an identity(principal) + to be used when reconciling this cluster + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead + of an entire object, this string should contain a valid + JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container + within a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that + triggered the event) or if no container name is specified + "spec.containers[2]" (container with index 2 in this + pod). This syntax is chosen only to have some well-defined + way of referencing a part of an object. TODO: this design + is not final and this field is subject to change in + the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object networkSpec: description: NetworkSpec encapsulates all things related to OCI network. diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 363c3b35..2e125a0a 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -14,6 +14,7 @@ resources: - bases/infrastructure.cluster.x-k8s.io_ocimanagedmachinepools.yaml - bases/infrastructure.cluster.x-k8s.io_ocimanagedcontrolplanes.yaml - bases/infrastructure.cluster.x-k8s.io_ocimanagedclusters.yaml +- bases/infrastructure.cluster.x-k8s.io_ociclusteridentities.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 1fa827e6..2aa8e222 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -222,4 +222,12 @@ rules: verbs: - get - patch - - update \ No newline at end of file + - update +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - ociclusteridentities + verbs: + - get + - list + - watch \ No newline at end of file diff --git a/controllers/ocicluster_controller.go b/controllers/ocicluster_controller.go index eb3a3a52..45153b41 100644 --- a/controllers/ocicluster_controller.go +++ b/controllers/ocicluster_controller.go @@ -25,6 +25,7 @@ import ( "github.com/oracle/cluster-api-provider-oci/api/v1beta1" infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" "github.com/oracle/cluster-api-provider-oci/cloud/scope" + cloudutil "github.com/oracle/cluster-api-provider-oci/cloud/util" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -80,13 +81,6 @@ func (r *OCIClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) } return ctrl.Result{}, err } - regionOverride := r.Region - if len(ociCluster.Spec.Region) > 0 { - regionOverride = ociCluster.Spec.Region - } - if len(regionOverride) <= 0 { - return ctrl.Result{}, errors.New("OCIClusterReconciler RegionIdentifier can't be nil") - } // Fetch the Cluster. cluster, err := util.GetOwnerCluster(ctx, r.Client, ociCluster.ObjectMeta) @@ -107,30 +101,29 @@ func (r *OCIClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) } var clusterScope scope.ClusterScopeClient - - clients, err := r.ClientProvider.GetOrBuildClient(regionOverride) + var clients scope.OCIClients + var clientProvider *scope.ClientProvider + clusterAccessor := scope.OCISelfManagedCluster{ + OCICluster: ociCluster, + } + clientProvider, clusterRegion, clients, err := cloudutil.InitClientsAndRegion(ctx, r.Client, r.Region, clusterAccessor, r.ClientProvider) if err != nil { - logger.Error(err, "Couldn't get the clients for region") + return ctrl.Result{}, err } - helper, err := patch.NewHelper(ociCluster, r.Client) if err != nil { return ctrl.Result{}, errors.Wrap(err, "failed to init patch helper") } - - clusterAccessor := scope.OCISelfManagedCluster{ - OCICluster: ociCluster, - } clusterScope, err = scope.NewClusterScope(scope.ClusterScopeParams{ Client: r.Client, Logger: &logger, Cluster: cluster, OCIClusterAccessor: clusterAccessor, - ClientProvider: r.ClientProvider, + ClientProvider: clientProvider, VCNClient: clients.VCNClient, LoadBalancerClient: clients.LoadBalancerClient, IdentityClient: clients.IdentityClient, - RegionIdentifier: regionOverride, + RegionIdentifier: clusterRegion, }) if err != nil { logger.Error(err, "Couldn't create cluster scope") diff --git a/controllers/ocimachine_controller.go b/controllers/ocimachine_controller.go index d0bd6a83..0e99c323 100644 --- a/controllers/ocimachine_controller.go +++ b/controllers/ocimachine_controller.go @@ -25,6 +25,7 @@ import ( infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" "github.com/oracle/cluster-api-provider-oci/cloud/ociutil" "github.com/oracle/cluster-api-provider-oci/cloud/scope" + cloudutil "github.com/oracle/cluster-api-provider-oci/cloud/util" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/core" "github.com/pkg/errors" @@ -115,18 +116,12 @@ func (r *OCIMachineReconciler) Reconcile(ctx context.Context, req ctrl.Request) r.Recorder.Eventf(ociMachine, corev1.EventTypeWarning, "ClusterNotAvailable", "Cluster is not available yet") return ctrl.Result{}, nil } - - regionOverride := r.Region - if len(ociCluster.Spec.Region) > 0 { - regionOverride = ociCluster.Spec.Region - } - if len(regionOverride) <= 0 { - return ctrl.Result{}, errors.New("OCIMachineReconciler RegionIdentifier can't be nil") + clusterAccessor := scope.OCISelfManagedCluster{ + OCICluster: ociCluster, } - - clients, err := r.ClientProvider.GetOrBuildClient(regionOverride) + _, _, clients, err := cloudutil.InitClientsAndRegion(ctx, r.Client, r.Region, clusterAccessor, r.ClientProvider) if err != nil { - logger.Error(err, "Couldn't get the clients for region") + return ctrl.Result{}, err } // Create the machine scope diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 7a8ebfdb..302604f1 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 00000000..31517f05 --- /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 of the field if not specified. +> 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/exp/api/v1beta1/ocimanagedcluster_types.go b/exp/api/v1beta1/ocimanagedcluster_types.go index 0b766b99..c0fbb48d 100644 --- a/exp/api/v1beta1/ocimanagedcluster_types.go +++ b/exp/api/v1beta1/ocimanagedcluster_types.go @@ -18,6 +18,7 @@ package v1beta1 import ( infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ) @@ -36,6 +37,10 @@ type OCIManagedClusterSpec struct { // +optional OCIResourceIdentifier string `json:"ociResourceIdentifier,omitempty"` + // IdentityRef is a reference to an identity(principal) to be used when reconciling this cluster + // +optional + IdentityRef *corev1.ObjectReference `json:"identityRef,omitempty"` + // NetworkSpec encapsulates all things related to OCI network. // +optional NetworkSpec infrastructurev1beta1.NetworkSpec `json:"networkSpec,omitempty"` diff --git a/exp/api/v1beta1/zz_generated.deepcopy.go b/exp/api/v1beta1/zz_generated.deepcopy.go index 93ed0424..76e5c8f6 100644 --- a/exp/api/v1beta1/zz_generated.deepcopy.go +++ b/exp/api/v1beta1/zz_generated.deepcopy.go @@ -23,6 +23,7 @@ package v1beta1 import ( apiv1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" + "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" cluster_apiapiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/errors" @@ -731,6 +732,11 @@ func (in *OCIManagedClusterList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OCIManagedClusterSpec) DeepCopyInto(out *OCIManagedClusterSpec) { *out = *in + if in.IdentityRef != nil { + in, out := &in.IdentityRef, &out.IdentityRef + *out = new(v1.ObjectReference) + **out = **in + } in.NetworkSpec.DeepCopyInto(&out.NetworkSpec) if in.FreeformTags != nil { in, out := &in.FreeformTags, &out.FreeformTags diff --git a/exp/controllers/ocimachinepool_controller.go b/exp/controllers/ocimachinepool_controller.go index 46ab6dac..f19df82b 100644 --- a/exp/controllers/ocimachinepool_controller.go +++ b/exp/controllers/ocimachinepool_controller.go @@ -25,6 +25,7 @@ import ( infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" "github.com/oracle/cluster-api-provider-oci/cloud/ociutil" "github.com/oracle/cluster-api-provider-oci/cloud/scope" + cloudutil "github.com/oracle/cluster-api-provider-oci/cloud/util" infrav1exp "github.com/oracle/cluster-api-provider-oci/exp/api/v1beta1" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/core" @@ -125,17 +126,12 @@ func (r *OCIMachinePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, nil } - regionOverride := r.Region - if len(ociCluster.Spec.Region) > 0 { - regionOverride = ociCluster.Spec.Region + clusterAccessor := scope.OCISelfManagedCluster{ + OCICluster: ociCluster, } - if len(regionOverride) <= 0 { - return ctrl.Result{}, errors.New("OCIMachinePoolReconciler RegionIdentifier can't be nil") - } - - clients, err := r.ClientProvider.GetOrBuildClient(regionOverride) + _, _, clients, err := cloudutil.InitClientsAndRegion(ctx, r.Client, r.Region, clusterAccessor, r.ClientProvider) if err != nil { - logger.Error(err, "Couldn't get the clients for region") + return ctrl.Result{}, err } // Create the machine pool scope diff --git a/exp/controllers/ocimanaged_machinepool_controller.go b/exp/controllers/ocimanaged_machinepool_controller.go index 78c58d05..89e48bb4 100644 --- a/exp/controllers/ocimanaged_machinepool_controller.go +++ b/exp/controllers/ocimanaged_machinepool_controller.go @@ -25,6 +25,7 @@ import ( infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" "github.com/oracle/cluster-api-provider-oci/cloud/ociutil" "github.com/oracle/cluster-api-provider-oci/cloud/scope" + cloudutil "github.com/oracle/cluster-api-provider-oci/cloud/util" infrav1exp "github.com/oracle/cluster-api-provider-oci/exp/api/v1beta1" "github.com/oracle/oci-go-sdk/v65/common" oke "github.com/oracle/oci-go-sdk/v65/containerengine" @@ -125,17 +126,12 @@ func (r *OCIManagedMachinePoolReconciler) Reconcile(ctx context.Context, req ctr return ctrl.Result{}, nil } - regionOverride := r.Region - if len(ociManagedCluster.Spec.Region) > 0 { - regionOverride = ociManagedCluster.Spec.Region + clusterAccessor := scope.OCIManagedCluster{ + OCIManagedCluster: ociManagedCluster, } - if len(regionOverride) <= 0 { - return ctrl.Result{}, errors.New("OCIMachinePoolReconciler RegionIdentifier can't be nil") - } - - clients, err := r.ClientProvider.GetOrBuildClient(regionOverride) + _, _, clients, err := cloudutil.InitClientsAndRegion(ctx, r.Client, r.Region, clusterAccessor, r.ClientProvider) if err != nil { - logger.Error(err, "Couldn't get the clients for region") + return ctrl.Result{}, err } controlPlane := &infrav1exp.OCIManagedControlPlane{} diff --git a/exp/controllers/ocimanagedcluster_controller.go b/exp/controllers/ocimanagedcluster_controller.go index 50311c55..824c8769 100644 --- a/exp/controllers/ocimanagedcluster_controller.go +++ b/exp/controllers/ocimanagedcluster_controller.go @@ -24,6 +24,7 @@ import ( "github.com/go-logr/logr" infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1" "github.com/oracle/cluster-api-provider-oci/cloud/scope" + cloudutil "github.com/oracle/cluster-api-provider-oci/cloud/util" infrav1exp "github.com/oracle/cluster-api-provider-oci/exp/api/v1beta1" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -80,13 +81,6 @@ func (r *OCIManagedClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re } return ctrl.Result{}, err } - regionOverride := r.Region - if len(ociCluster.Spec.Region) > 0 { - regionOverride = ociCluster.Spec.Region - } - if len(regionOverride) <= 0 { - return ctrl.Result{}, errors.New("OCIManagedClusterReconciler RegionIdentifier can't be nil") - } // Fetch the Cluster. cluster, err := util.GetOwnerCluster(ctx, r.Client, ociCluster.ObjectMeta) @@ -112,11 +106,12 @@ func (r *OCIManagedClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re return ctrl.Result{}, nil } - var clusterScope scope.ClusterScopeClient - - clients, err := r.ClientProvider.GetOrBuildClient(regionOverride) + clusterAccessor := scope.OCIManagedCluster{ + OCIManagedCluster: ociCluster, + } + clientProvider, clusterRegion, clients, err := cloudutil.InitClientsAndRegion(ctx, r.Client, r.Region, clusterAccessor, r.ClientProvider) if err != nil { - logger.Error(err, "Couldn't get the clients for region") + return ctrl.Result{}, err } helper, err := patch.NewHelper(ociCluster, r.Client) @@ -124,19 +119,16 @@ func (r *OCIManagedClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re return ctrl.Result{}, errors.Wrap(err, "failed to init patch helper") } - clusterAccessor := scope.OCIManagedCluster{ - OCIManagedCluster: ociCluster, - } - clusterScope, err = scope.NewClusterScope(scope.ClusterScopeParams{ + clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ Client: r.Client, Logger: &logger, Cluster: cluster, OCIClusterAccessor: clusterAccessor, - ClientProvider: r.ClientProvider, + ClientProvider: clientProvider, VCNClient: clients.VCNClient, LoadBalancerClient: clients.LoadBalancerClient, IdentityClient: clients.IdentityClient, - RegionIdentifier: regionOverride, + RegionIdentifier: clusterRegion, }) if err != nil { logger.Error(err, "Couldn't create cluster scope") diff --git a/exp/controllers/ocimanagedcluster_controlplane_controller.go b/exp/controllers/ocimanagedcluster_controlplane_controller.go index c60a7385..cb2d76cd 100644 --- a/exp/controllers/ocimanagedcluster_controlplane_controller.go +++ b/exp/controllers/ocimanagedcluster_controlplane_controller.go @@ -24,6 +24,7 @@ import ( "github.com/go-logr/logr" "github.com/oracle/cluster-api-provider-oci/cloud/ociutil" "github.com/oracle/cluster-api-provider-oci/cloud/scope" + cloudutil "github.com/oracle/cluster-api-provider-oci/cloud/util" infrav1exp "github.com/oracle/cluster-api-provider-oci/exp/api/v1beta1" "github.com/oracle/oci-go-sdk/v65/containerengine" "github.com/pkg/errors" @@ -124,17 +125,12 @@ func (r *OCIManagedClusterControlPlaneReconciler) Reconcile(ctx context.Context, return ctrl.Result{}, nil } - regionOverride := r.Region - if len(ociManagedCluster.Spec.Region) > 0 { - regionOverride = ociManagedCluster.Spec.Region - } - if len(regionOverride) <= 0 { - return ctrl.Result{}, errors.New("OCIManagedControlPlane RegionIdentifier can't be nil") + clusterAccessor := scope.OCIManagedCluster{ + OCIManagedCluster: ociManagedCluster, } - - clients, err := r.ClientProvider.GetOrBuildClient(regionOverride) + clientProvider, clusterRegion, clients, err := cloudutil.InitClientsAndRegion(ctx, r.Client, r.Region, clusterAccessor, r.ClientProvider) if err != nil { - logger.Error(err, "Couldn't get the clients for region") + return ctrl.Result{}, err } helper, err := patch.NewHelper(controlPlane, r.Client) @@ -154,17 +150,14 @@ func (r *OCIManagedClusterControlPlaneReconciler) Reconcile(ctx context.Context, var controlPlaneScope *scope.ManagedControlPlaneScope - clusterAccessor := scope.OCIManagedCluster{ - OCIManagedCluster: ociManagedCluster, - } controlPlaneScope, err = scope.NewManagedControlPlaneScope(scope.ManagedControlPlaneScopeParams{ Client: r.Client, Logger: &logger, Cluster: cluster, OCIClusterAccessor: clusterAccessor, - ClientProvider: r.ClientProvider, + ClientProvider: clientProvider, ContainerEngineClient: clients.ContainerEngineClient, - RegionIdentifier: regionOverride, + RegionIdentifier: clusterRegion, OCIManagedControlPlane: controlPlane, BaseClient: clients.BaseClient, }) diff --git a/test/e2e/cluster_test.go b/test/e2e/cluster_test.go index 37d376c0..1b442858 100644 --- a/test/e2e/cluster_test.go +++ b/test/e2e/cluster_test.go @@ -121,8 +121,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, CNIManifestPath: e2eConfig.GetVariable(capi_e2e.CNIPath), WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), @@ -144,8 +144,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(3), - WorkerMachineCount: pointer.Int64Ptr(0), + ControlPlaneMachineCount: pointer.Int64(3), + WorkerMachineCount: pointer.Int64(0), }, WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane"), @@ -167,8 +167,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane"), @@ -189,8 +189,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane"), @@ -212,8 +212,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane"), @@ -297,8 +297,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane"), @@ -329,8 +329,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane"), @@ -361,8 +361,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane"), @@ -387,8 +387,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster-bare-metal"), WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane-bare-metal"), @@ -412,8 +412,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane"), @@ -435,8 +435,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, CNIManifestPath: e2eConfig.GetVariable(capi_e2e.CNIPath), WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), @@ -458,8 +458,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, CNIManifestPath: e2eConfig.GetVariable(capi_e2e.CNIPath), WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), @@ -481,8 +481,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, CNIManifestPath: e2eConfig.GetVariable(capi_e2e.CNIPath), WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), @@ -504,8 +504,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, CNIManifestPath: e2eConfig.GetVariable(capi_e2e.CNIPath), WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), @@ -527,8 +527,8 @@ var _ = Describe("Workload cluster creation", func() { Namespace: namespace.Name, ClusterName: clusterName, KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), }, CNIManifestPath: e2eConfig.GetVariable(capi_e2e.CNIPath), WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), @@ -555,6 +555,29 @@ var _ = Describe("Workload cluster creation", func() { WaitForMachinePoolToScale: e2eConfig.GetIntervals(specName, "wait-machine-pool-nodes"), }) }) + + 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, + ConfigCluster: clusterctl.ConfigClusterInput{ + LogFolder: filepath.Join(artifactFolder, "clusters", bootstrapClusterProxy.GetName()), + ClusterctlConfigPath: clusterctlConfigPath, + KubeconfigPath: bootstrapClusterProxy.GetKubeconfigPath(), + InfrastructureProvider: clusterctl.DefaultInfrastructureProvider, + Flavor: "cluster-identity", + Namespace: namespace.Name, + ClusterName: clusterName, + KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), + }, + CNIManifestPath: e2eConfig.GetVariable(capi_e2e.CNIPath), + WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), + WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane"), + WaitForMachineDeployments: e2eConfig.GetIntervals(specName, "wait-worker-nodes"), + }, result) + }) }) func verifyMultipleNsgSubnet(ctx context.Context, namespace string, clusterName string, mcDeployments []*clusterv1.MachineDeployment) { diff --git a/test/e2e/config/e2e_conf.yaml b/test/e2e/config/e2e_conf.yaml index 145cabfb..3c7a6419 100644 --- a/test/e2e/config/e2e_conf.yaml +++ b/test/e2e/config/e2e_conf.yaml @@ -71,6 +71,8 @@ providers: - sourcePath: "../data/infrastructure-oci/v1beta1/cluster-template-externally-managed-vcn.yaml" - sourcePath: "../data/infrastructure-oci/v1beta1/cluster-template-machine-pool.yaml" - sourcePath: "../data/infrastructure-oci/v1beta1/cluster-template-managed.yaml" + - sourcePath: "../data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity.yaml" + - sourcePath: "../data/infrastructure-oci/v1beta1/cluster-template-cluster-identity.yaml" - sourcePath: "../data/infrastructure-oci/v1beta1/metadata.yaml" variables: diff --git a/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-cluster-identity/cluster-identity.yaml b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-cluster-identity/cluster-identity.yaml new file mode 100644 index 00000000..3b76b1e6 --- /dev/null +++ b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-cluster-identity/cluster-identity.yaml @@ -0,0 +1,22 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OCIClusterIdentity +metadata: + name: cluster-identity-user-principal +spec: + type: UserPrincipal + principalSecret: + name: user-credentials + namespace: "${NAMESPACE}" + allowedNamespaces: {} +--- +apiVersion: v1 +kind: Secret +metadata: + name: user-credentials +type: Opaque +data: + tenancy: "${OCI_TENANCY_ID_B64}" + user: "${OCI_USER_ID_B64}" + key: "${OCI_CREDENTIALS_KEY_B64}" + region: "${OCI_REGION_B64}" + fingerprint: "${OCI_CREDENTIALS_FINGERPRINT_B64}" \ No newline at end of file diff --git a/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-cluster-identity/cluster.yaml b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-cluster-identity/cluster.yaml new file mode 100644 index 00000000..f515abc9 --- /dev/null +++ b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-cluster-identity/cluster.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OCICluster +metadata: + name: "${CLUSTER_NAME}" +spec: + identityRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OCIClusterIdentity + name: cluster-identity-user-principal + namespace: "${NAMESPACE}" diff --git a/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-cluster-identity/kustomization.yaml b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-cluster-identity/kustomization.yaml new file mode 100644 index 00000000..878b30f4 --- /dev/null +++ b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-cluster-identity/kustomization.yaml @@ -0,0 +1,9 @@ +bases: + - ../bases/cluster.yaml + - ../bases/md.yaml + - ../bases/crs.yaml + - ../bases/ccm.yaml + - ./cluster-identity.yaml + +patchesStrategicMerge: + - ./cluster.yaml \ No newline at end of file diff --git a/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/cluster-identity.yaml b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/cluster-identity.yaml new file mode 100644 index 00000000..c6dce9e5 --- /dev/null +++ b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/cluster-identity.yaml @@ -0,0 +1,21 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OCIClusterIdentity +metadata: + name: cluster-identity-user-principal +spec: + type: UserPrincipal + principalSecret: + name: user-credentials + namespace: "${NAMESPACE}" + allowedNamespaces: {} +--- +apiVersion: v1 +kind: Secret +metadata: + name: user-credentials +type: Opaque +data: + tenancy: "${OCI_TENANCY_ID_B64}" + user: "${OCI_USER_ID_B64}" + key: "${OCI_CREDENTIALS_KEY_B64}" + fingerprint: "${OCI_CREDENTIALS_FINGERPRINT_B64}" \ No newline at end of file diff --git a/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/cluster.yaml b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/cluster.yaml new file mode 100644 index 00000000..128925e6 --- /dev/null +++ b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/cluster.yaml @@ -0,0 +1,40 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + labels: + cluster.x-k8s.io/cluster-name: "${CLUSTER_NAME}" + name: "${CLUSTER_NAME}" + namespace: "${NAMESPACE}" +spec: + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OCIManagedCluster + name: "${CLUSTER_NAME}" + namespace: "${NAMESPACE}" + controlPlaneRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OCIManagedControlPlane + name: "${CLUSTER_NAME}" + namespace: "${NAMESPACE}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OCIManagedCluster +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-user-principal + namespace: "${NAMESPACE}" +--- +kind: OCIManagedControlPlane +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +metadata: + name: "${CLUSTER_NAME}" + namespace: "${NAMESPACE}" +spec: + version: "${OCI_MANAGED_KUBERNETES_VERSION}" \ No newline at end of file diff --git a/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/kustomization.yaml b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/kustomization.yaml new file mode 100644 index 00000000..ad6508dd --- /dev/null +++ b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/kustomization.yaml @@ -0,0 +1,5 @@ +bases: + - ./cluster.yaml + - ./machine-pool.yaml + - ./cluster-identity.yaml + diff --git a/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/machine-pool.yaml b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/machine-pool.yaml new file mode 100644 index 00000000..21d7b23b --- /dev/null +++ b/test/e2e/data/infrastructure-oci/v1beta1/cluster-template-managed-cluster-identity/machine-pool.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachinePool +metadata: + name: ${CLUSTER_NAME}-mp-0 + namespace: default +spec: + clusterName: ${CLUSTER_NAME} + replicas: ${NODE_MACHINE_COUNT} + template: + spec: + clusterName: ${CLUSTER_NAME} + bootstrap: + dataSecretName: "" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: OCIManagedMachinePool + name: ${CLUSTER_NAME}-mp-0 + version: ${KUBERNETES_VERSION} +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: OCIManagedMachinePool +metadata: + name: ${CLUSTER_NAME}-mp-0 + namespace: default +spec: + version: "${OCI_MANAGED_KUBERNETES_VERSION}" + nodeShape: "${OCI_MANAGED_NODE_SHAPE}" + sshPublicKey: "${OCI_SSH_KEY}" + nodeMetadata: + user_data: "IyEvYmluL2Jhc2gKY3VybCAtLWZhaWwgLUggIkF1dGhvcml6YXRpb246IEJlYXJlciBPcmFjbGUiIC1MMCBodHRwOi8vMTY5LjI1NC4xNjkuMjU0L29wYy92Mi9pbnN0YW5jZS9tZXRhZGF0YS9va2VfaW5pdF9zY3JpcHQgfCBiYXNlNjQgLS1kZWNvZGUgPi92YXIvcnVuL29rZS1pbml0LnNoCnByb3ZpZGVyX2lkPSQoY3VybCAtLWZhaWwgLUggIkF1dGhvcml6YXRpb246IEJlYXJlciBPcmFjbGUiIC1MMCBodHRwOi8vMTY5LjI1NC4xNjkuMjU0L29wYy92Mi9pbnN0YW5jZS9pZCkKYmFzaCAvdmFyL3J1bi9va2UtaW5pdC5zaCAtLWt1YmVsZXQtZXh0cmEtYXJncyAiLS1wcm92aWRlci1pZD1vY2k6Ly8kcHJvdmlkZXJfaWQiCg==" + nodeSourceViaImage: + imageId: "${OCI_MANAGED_NODE_IMAGE_ID}" + bootVolumeSizeInGBs: 50 + nodeShapeConfig: + memoryInGBs: "16" + ocpus: "1" +--- \ No newline at end of file diff --git a/test/e2e/managed_cluster_test.go b/test/e2e/managed_cluster_test.go index f4991455..83db17d6 100644 --- a/test/e2e/managed_cluster_test.go +++ b/test/e2e/managed_cluster_test.go @@ -174,6 +174,48 @@ var _ = Describe("Managed Workload cluster creation", func() { upgradeControlPlaneVersionSpec(ctx, bootstrapClusterProxy.GetClient(), clusterName, namespace.Name, e2eConfig.GetIntervals(specName, "wait-control-plane")) }) + + It("Managed Cluster - Cluster Identity", func() { + clusterName = getClusterName(clusterNamePrefix, "cls-iden") + input := clusterctl.ApplyClusterTemplateAndWaitInput{ + ClusterProxy: bootstrapClusterProxy, + ConfigCluster: clusterctl.ConfigClusterInput{ + LogFolder: filepath.Join(artifactFolder, "clusters", bootstrapClusterProxy.GetName()), + ClusterctlConfigPath: clusterctlConfigPath, + KubeconfigPath: bootstrapClusterProxy.GetKubeconfigPath(), + InfrastructureProvider: clusterctl.DefaultInfrastructureProvider, + Flavor: "managed-cluster-identity", + Namespace: namespace.Name, + ClusterName: clusterName, + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), + KubernetesVersion: e2eConfig.GetVariable(capi_e2e.KubernetesVersion), + }, + WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), + WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane"), + WaitForMachinePools: e2eConfig.GetIntervals(specName, "wait-machine-pool-nodes"), + WaitForMachineDeployments: e2eConfig.GetIntervals(specName, "wait-worker-nodes"), + } + input.WaitForControlPlaneInitialized = func(ctx context.Context, input clusterctl.ApplyClusterTemplateAndWaitInput, result *clusterctl.ApplyClusterTemplateAndWaitResult) { + Expect(ctx).NotTo(BeNil(), "ctx is required for DiscoveryAndWaitForControlPlaneInitialized") + lister := input.ClusterProxy.GetClient() + Expect(lister).ToNot(BeNil(), "Invalid argument. input.Lister can't be nil when calling DiscoveryAndWaitForControlPlaneInitialized") + var controlPlane *infrav1exp.OCIManagedControlPlane + Eventually(func(g Gomega) { + controlPlane = GetOCIManagedControlPlaneByCluster(ctx, lister, result.Cluster.Name, result.Cluster.Namespace) + if controlPlane != nil { + Log(fmt.Sprintf("Control plane is not nil, status is %t", controlPlane.Status.Ready)) + } + g.Expect(controlPlane).ToNot(BeNil()) + g.Expect(controlPlane.Status.Ready).To(BeTrue()) + }, input.WaitForControlPlaneIntervals...).Should(Succeed(), "Couldn't get the control plane ready status for the cluster %s", klog.KObj(result.Cluster)) + } + input.WaitForControlPlaneMachinesReady = func(ctx context.Context, input clusterctl.ApplyClusterTemplateAndWaitInput, result *clusterctl.ApplyClusterTemplateAndWaitResult) { + // Not applicable + } + + clusterctl.ApplyClusterTemplateAndWait(ctx, input, result) + }) }) // GetKubeadmControlPlaneByCluster returns the KubeadmControlPlane objects for a cluster.