diff --git a/PROJECT b/PROJECT index cf4ad92d80..3097a14aee 100644 --- a/PROJECT +++ b/PROJECT @@ -95,4 +95,13 @@ resources: kind: AtlasSearchIndexConfigs path: github.com/mongodb/mongodb-atlas-kubernetes/api/v1 version: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: mongodb.com + group: atlas + kind: AtlasPrivateEndpoint + path: github.com/mongodb/mongodb-atlas-kubernetes/api/v1 + version: v1 version: "3" diff --git a/config/crd/bases/atlas.mongodb.com_atlasprivateendpoints.yaml b/config/crd/bases/atlas.mongodb.com_atlasprivateendpoints.yaml new file mode 100644 index 0000000000..66861afa19 --- /dev/null +++ b/config/crd/bases/atlas.mongodb.com_atlasprivateendpoints.yaml @@ -0,0 +1,310 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: atlasprivateendpoints.atlas.mongodb.com +spec: + group: atlas.mongodb.com + names: + categories: + - atlas + kind: AtlasPrivateEndpoint + listKind: AtlasPrivateEndpointList + plural: atlasprivateendpoints + shortNames: + - pe + singular: atlasprivateendpoint + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.provider + name: Provider + type: string + - jsonPath: .spec.region + name: Region + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + name: v1 + schema: + openAPIV3Schema: + description: |- + The AtlasPrivateEndpoint custom resource definition (CRD) defines a desired [Private Endpoint](https://www.mongodb.com/docs/atlas/security-private-endpoint/#std-label-private-endpoint-overview) configuration for an Atlas project. + It allows a private connection between your cloud provider and Atlas that doesn't send information through a public network. + + You can use private endpoints to create a unidirectional connection to Atlas clusters from your virtual network. + 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: AtlasPrivateEndpointSpec is the specification of the desired + configuration of a project private endpoint + properties: + awsConfiguration: + description: AWSConfiguration is the specific AWS settings for the + private endpoint + items: + description: AWSPrivateEndpointConfiguration holds the AWS configuration + done on customer network + properties: + id: + description: ID that identifies the private endpoint's network + interface that someone added to this private endpoint service. + type: string + required: + - id + type: object + type: array + azureConfiguration: + description: AzureConfiguration is the specific Azure settings for + the private endpoint + items: + description: AzurePrivateEndpointConfiguration holds the Azure configuration + done on customer network + properties: + id: + description: ID that identifies the private endpoint's network + interface that someone added to this private endpoint service. + type: string + ipAddress: + description: IP address of the private endpoint in your Azure + VNet that someone added to this private endpoint service. + type: string + required: + - id + - ipAddress + type: object + type: array + connectionSecret: + description: LocalObjectReference is a reference to an object in the + same namespace as the referent + properties: + name: + description: |- + Name of the resource being referred to + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object + externalProjectRef: + description: ExternalProject holds the Atlas project ID the user belongs + to + properties: + id: + description: ID is the Atlas project ID + type: string + required: + - id + type: object + gcpConfiguration: + description: GCPConfiguration is the specific Google Cloud settings + for the private endpoint + items: + description: GCPPrivateEndpointConfiguration holds the GCP configuration + done on customer network + properties: + endpoints: + description: Endpoints is the list of individual private endpoints + that comprise this endpoint group. + items: + description: GCPPrivateEndpoint holds the GCP forwarding rules + configured on customer network + properties: + ipAddress: + description: IP address to which this Google Cloud consumer + forwarding rule resolves. + type: string + name: + description: Name that identifies the Google Cloud consumer + forwarding rule that you created. + type: string + required: + - ipAddress + - name + type: object + type: array + groupName: + description: GroupName is the label that identifies a set of + endpoints. + type: string + projectId: + description: ProjectID that identifies the Google Cloud project + in which you created the endpoints. + type: string + required: + - endpoints + - groupName + - projectId + type: object + type: array + projectRef: + description: Project is a reference to AtlasProject resource the user + belongs to + properties: + name: + description: Name is the name of the Kubernetes Resource + type: string + namespace: + description: Namespace is the namespace of the Kubernetes Resource + type: string + required: + - name + type: object + provider: + description: Name of the cloud service provider for which you want + to create the private endpoint service. + enum: + - AWS + - GCP + - AZURE + type: string + region: + description: Region of the chosen cloud provider in which you want + to create the private endpoint service. + type: string + required: + - provider + - region + type: object + x-kubernetes-validations: + - message: must define only one project reference through externalProjectRef + or projectRef + rule: (has(self.externalProjectRef) && !has(self.projectRef)) || (!has(self.externalProjectRef) + && has(self.projectRef)) + - message: must define a local connection secret when referencing an external + project + rule: (has(self.externalProjectRef) && has(self.connectionSecret)) || + !has(self.externalProjectRef) + status: + description: AtlasPrivateEndpointStatus is the most recent observed status + of the AtlasPrivateEndpoint cluster. Read-only. + properties: + conditions: + description: Conditions is the list of statuses showing the current + state of the Atlas Custom Resource + items: + description: Condition describes the state of an Atlas Custom Resource + at a certain point. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of Atlas Custom Resource condition. + type: string + required: + - status + - type + type: object + type: array + endpoints: + description: Endpoints are the status of the endpoints connected to + the service + items: + description: EndpointInterfaceStatus is the most recent observed + status the interfaces attached to the configured service. Read-only. + properties: + ID: + description: ID is the external identifier set on the specification + to configure the interface + type: string + InterfaceStatus: + description: InterfaceStatus is the state of the private endpoint + interface + type: string + connectionName: + description: ConnectionName is the label that Atlas generates + that identifies the Azure private endpoint connection + type: string + error: + description: Error is the description of the failure occurred + when configuring the private endpoint + type: string + gcpForwardingRules: + description: GCPForwardingRules is the status of the customer + GCP private endpoint(forwarding rules) + items: + description: GCPForwardingRule is the most recent observed + status the GCP forwarding rules configured for an interface. + Read-only. + properties: + name: + type: string + status: + type: string + type: object + type: array + type: object + type: array + error: + description: Error is the description of the failure occurred when + configuring the private endpoint + type: string + observedGeneration: + description: |- + ObservedGeneration indicates the generation of the resource specification that the Atlas Operator is aware of. + The Atlas Operator updates this field to the 'metadata.generation' as soon as it starts reconciliation of the resource. + format: int64 + type: integer + resourceId: + description: ResourceID is the root-relative path that identifies + of the Atlas Azure Private Link Service + type: string + serviceAttachmentNames: + description: ServiceAttachmentNames is the list of URLs that identifies + endpoints that Atlas can use to access one service across the private + connection + items: + type: string + type: array + serviceId: + description: ServiceID is the unique identifier of the private endpoint + service in Atlas + type: string + serviceName: + description: ServiceName is the unique identifier of the Amazon Web + Services (AWS) PrivateLink endpoint service or Azure Private Link + Service managed by Atlas + type: string + serviceStatus: + description: ServiceStatus is the state of the private endpoint service + type: string + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 56d9515628..24a3953fbe 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -14,5 +14,6 @@ resources: - bases/atlas.mongodb.com_atlasstreamconnections.yaml - bases/atlas.mongodb.com_atlassearchindexconfigs.yaml - bases/atlas.mongodb.com_atlasbackupcompliancepolicies.yaml + - bases/atlas.mongodb.com_atlasprivateendpoints.yaml configurations: - kustomizeconfig.yaml diff --git a/config/crd/patches/cainjection_in_atlasprivateendpoints.yaml b/config/crd/patches/cainjection_in_atlasprivateendpoints.yaml new file mode 100644 index 0000000000..e0d24385f4 --- /dev/null +++ b/config/crd/patches/cainjection_in_atlasprivateendpoints.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: atlasprivateendpoints.atlas.mongodb.com diff --git a/config/crd/patches/webhook_in_atlasprivateendpoints.yaml b/config/crd/patches/webhook_in_atlasprivateendpoints.yaml new file mode 100644 index 0000000000..080471aa6a --- /dev/null +++ b/config/crd/patches/webhook_in_atlasprivateendpoints.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: atlasprivateendpoints.atlas.mongodb.com +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/rbac/atlasprivateendpoint_editor_role.yaml b/config/rbac/atlasprivateendpoint_editor_role.yaml new file mode 100644 index 0000000000..80c8c1bc31 --- /dev/null +++ b/config/rbac/atlasprivateendpoint_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit atlasprivateendpoints. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: atlasprivateendpoint-editor-role +rules: +- apiGroups: + - atlas.mongodb.com + resources: + - atlasprivateendpoints + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - atlas.mongodb.com + resources: + - atlasprivateendpoints/status + verbs: + - get diff --git a/config/rbac/atlasprivateendpoint_viewer_role.yaml b/config/rbac/atlasprivateendpoint_viewer_role.yaml new file mode 100644 index 0000000000..a29ab3c5d1 --- /dev/null +++ b/config/rbac/atlasprivateendpoint_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view atlasprivateendpoints. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: atlasprivateendpoint-viewer-role +rules: +- apiGroups: + - atlas.mongodb.com + resources: + - atlasprivateendpoints + verbs: + - get + - list + - watch +- apiGroups: + - atlas.mongodb.com + resources: + - atlasprivateendpoints/status + verbs: + - get diff --git a/config/samples/atlas_v1_atlasprivateendpoint.yaml b/config/samples/atlas_v1_atlasprivateendpoint.yaml new file mode 100644 index 0000000000..5c85b9bffd --- /dev/null +++ b/config/samples/atlas_v1_atlasprivateendpoint.yaml @@ -0,0 +1,12 @@ +apiVersion: atlas.mongodb.com/v1 +kind: AtlasPrivateEndpoint +metadata: + name: atlasprivateendpoint-sample +spec: + projectRef: + name: my-project + provider: AWS + region: EU_CENTRAL_1 + awsConfiguration: + id: vpce-f4k34w51d + diff --git a/pkg/api/v1/atlascustomresource.go b/pkg/api/v1/atlascustomresource.go index 0ae044082f..85bccf747d 100644 --- a/pkg/api/v1/atlascustomresource.go +++ b/pkg/api/v1/atlascustomresource.go @@ -29,6 +29,7 @@ var _ AtlasCustomResource = &AtlasStreamInstance{} var _ AtlasCustomResource = &AtlasStreamConnection{} var _ AtlasCustomResource = &AtlasSearchIndexConfig{} var _ AtlasCustomResource = &AtlasBackupCompliancePolicy{} +var _ AtlasCustomResource = &AtlasPrivateEndpoint{} // InitCondition initializes the underlying type of the given condition to the given default value. func InitCondition(resource AtlasCustomResource, defaultCondition api.Condition) []api.Condition { diff --git a/pkg/api/v1/atlasdatabaseuser_types_test.go b/pkg/api/v1/atlasdatabaseuser_types_test.go index 8d03809786..d216acab28 100644 --- a/pkg/api/v1/atlasdatabaseuser_types_test.go +++ b/pkg/api/v1/atlasdatabaseuser_types_test.go @@ -8,7 +8,7 @@ import ( ) func TestProjectReference(t *testing.T) { - tests := projectReferenceTestCase{ + tests := celTestCase{ "no project reference is set": { object: &AtlasDatabaseUser{ Spec: AtlasDatabaseUserSpec{}, @@ -58,7 +58,7 @@ func TestProjectReference(t *testing.T) { } func TestExternalProjectReferenceConnectionSecret(t *testing.T) { - tests := projectReferenceTestCase{ + tests := celTestCase{ "external project references is set without connection secret": { object: &AtlasDatabaseUser{ Spec: AtlasDatabaseUserSpec{ diff --git a/pkg/api/v1/atlasdeployment_types_test.go b/pkg/api/v1/atlasdeployment_types_test.go index e047533688..892261c5fc 100644 --- a/pkg/api/v1/atlasdeployment_types_test.go +++ b/pkg/api/v1/atlasdeployment_types_test.go @@ -8,7 +8,7 @@ import ( ) func TestDeploymentProjectReference(t *testing.T) { - tests := projectReferenceTestCase{ + tests := celTestCase{ "no project reference is set": { object: &AtlasDeployment{ Spec: AtlasDeploymentSpec{}, @@ -58,7 +58,7 @@ func TestDeploymentProjectReference(t *testing.T) { } func TestDeploymentExternalProjectReferenceConnectionSecret(t *testing.T) { - tests := projectReferenceTestCase{ + tests := celTestCase{ "external project references is set without connection secret": { object: &AtlasDeployment{ Spec: AtlasDeploymentSpec{ diff --git a/pkg/api/v1/atlasprivateendpoint_types.go b/pkg/api/v1/atlasprivateendpoint_types.go new file mode 100644 index 0000000000..4d66426c5d --- /dev/null +++ b/pkg/api/v1/atlasprivateendpoint_types.go @@ -0,0 +1,161 @@ +/* +Copyright 2024 MongoDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/kube" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status" +) + +func init() { + SchemeBuilder.Register(&AtlasPrivateEndpoint{}, &AtlasPrivateEndpointList{}) +} + +// +kubebuilder:validation:XValidation:rule="(has(self.externalProjectRef) && !has(self.projectRef)) || (!has(self.externalProjectRef) && has(self.projectRef))",message="must define only one project reference through externalProjectRef or projectRef" +// +kubebuilder:validation:XValidation:rule="(has(self.externalProjectRef) && has(self.connectionSecret)) || !has(self.externalProjectRef)",message="must define a local connection secret when referencing an external project" + +// AtlasPrivateEndpointSpec is the specification of the desired configuration of a project private endpoint +type AtlasPrivateEndpointSpec struct { + // Project is a reference to AtlasProject resource the user belongs to + // +kubebuilder:validation:Optional + Project *common.ResourceRefNamespaced `json:"projectRef,omitempty"` + // ExternalProject holds the Atlas project ID the user belongs to + // +kubebuilder:validation:Optional + ExternalProject *ExternalProjectReference `json:"externalProjectRef,omitempty"` + + // Local credentials + api.LocalCredentialHolder `json:",inline"` + + // Name of the cloud service provider for which you want to create the private endpoint service. + // +kubebuilder:validation:Enum=AWS;GCP;AZURE + // +kubebuilder:validation:Required + Provider string `json:"provider"` + // Region of the chosen cloud provider in which you want to create the private endpoint service. + // +kubebuilder:validation:Required + Region string `json:"region"` + // AWSConfiguration is the specific AWS settings for the private endpoint + // +kubebuilder:validation:Optional + AWSConfiguration []AWSPrivateEndpointConfiguration `json:"awsConfiguration,omitempty"` + // AzureConfiguration is the specific Azure settings for the private endpoint + // +kubebuilder:validation:Optional + AzureConfiguration []AzurePrivateEndpointConfiguration `json:"azureConfiguration,omitempty"` + // GCPConfiguration is the specific Google Cloud settings for the private endpoint + // +kubebuilder:validation:Optional + GCPConfiguration []GCPPrivateEndpointConfiguration `json:"gcpConfiguration,omitempty"` +} + +// AWSPrivateEndpointConfiguration holds the AWS configuration done on customer network +type AWSPrivateEndpointConfiguration struct { + // ID that identifies the private endpoint's network interface that someone added to this private endpoint service. + // +kubebuilder:validation:Required + ID string `json:"id"` +} + +// AzurePrivateEndpointConfiguration holds the Azure configuration done on customer network +type AzurePrivateEndpointConfiguration struct { + // ID that identifies the private endpoint's network interface that someone added to this private endpoint service. + // +kubebuilder:validation:Required + ID string `json:"id"` + // IP address of the private endpoint in your Azure VNet that someone added to this private endpoint service. + // +kubebuilder:validation:Required + IP string `json:"ipAddress"` +} + +// GCPPrivateEndpointConfiguration holds the GCP configuration done on customer network +type GCPPrivateEndpointConfiguration struct { + // ProjectID that identifies the Google Cloud project in which you created the endpoints. + // +kubebuilder:validation:Required + ProjectID string `json:"projectId"` + // GroupName is the label that identifies a set of endpoints. + // +kubebuilder:validation:Required + GroupName string `json:"groupName"` + // Endpoints is the list of individual private endpoints that comprise this endpoint group. + // +kubebuilder:validation:Required + Endpoints []GCPPrivateEndpoint `json:"endpoints"` +} + +// GCPPrivateEndpoint holds the GCP forwarding rules configured on customer network +type GCPPrivateEndpoint struct { + // Name that identifies the Google Cloud consumer forwarding rule that you created. + // +kubebuilder:validation:Required + Name string `json:"name"` + // IP address to which this Google Cloud consumer forwarding rule resolves. + // +kubebuilder:validation:Required + IP string `json:"ipAddress"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +groupName:=atlas.mongodb.com +// +kubebuilder:resource:categories=atlas,shortName=pe +// +kubebuilder:printcolumn:name="Provider",type=string,JSONPath=`.spec.provider` +// +kubebuilder:printcolumn:name="Region",type=string,JSONPath=`.spec.region` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` + +// The AtlasPrivateEndpoint custom resource definition (CRD) defines a desired [Private Endpoint](https://www.mongodb.com/docs/atlas/security-private-endpoint/#std-label-private-endpoint-overview) configuration for an Atlas project. +// It allows a private connection between your cloud provider and Atlas that doesn't send information through a public network. +// +// You can use private endpoints to create a unidirectional connection to Atlas clusters from your virtual network. +type AtlasPrivateEndpoint struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AtlasPrivateEndpointSpec `json:"spec,omitempty"` + Status status.AtlasPrivateEndpointStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// AtlasPrivateEndpointList contains a list of AtlasPrivateEndpoint +type AtlasPrivateEndpointList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AtlasPrivateEndpoint `json:"items"` +} + +func (pe *AtlasPrivateEndpoint) AtlasProjectObjectKey() client.ObjectKey { + ns := pe.Namespace + if pe.Spec.Project.Namespace != "" { + ns = pe.Spec.Project.Namespace + } + + return kube.ObjectKey(ns, pe.Spec.Project.Name) +} + +func (pe *AtlasPrivateEndpoint) Credentials() *api.LocalObjectReference { + return pe.Spec.Credentials() +} + +func (pe *AtlasPrivateEndpoint) GetStatus() api.Status { + return pe.Status +} + +func (pe *AtlasPrivateEndpoint) UpdateStatus(conditions []api.Condition, options ...api.Option) { + pe.Status.Conditions = conditions + pe.Status.ObservedGeneration = pe.ObjectMeta.Generation + + for _, o := range options { + v := o.(status.AtlasPrivateEndpointStatusOption) + v(&pe.Status) + } +} diff --git a/pkg/api/v1/atlasprivateendpoint_types_test.go b/pkg/api/v1/atlasprivateendpoint_types_test.go new file mode 100644 index 0000000000..d774a04daf --- /dev/null +++ b/pkg/api/v1/atlasprivateendpoint_types_test.go @@ -0,0 +1,114 @@ +package v1 // nolint: dupl + +import ( + "testing" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" +) + +func TestPrivateEndpointProjectReference(t *testing.T) { + tests := celTestCase{ + "no project reference is set": { + object: &AtlasPrivateEndpoint{ + Spec: AtlasPrivateEndpointSpec{}, + }, + expectedErrors: []string{"spec: Invalid value: \"object\": must define only one project reference through externalProjectRef or projectRef"}, + }, + "both project references are set": { + object: &AtlasPrivateEndpoint{ + Spec: AtlasPrivateEndpointSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + ExternalProject: &ExternalProjectReference{ + ID: "my-project-id", + }, + }, + }, + expectedErrors: []string{ + "spec: Invalid value: \"object\": must define only one project reference through externalProjectRef or projectRef", + "spec: Invalid value: \"object\": must define a local connection secret when referencing an external project", + }, + }, + "external project references is set": { + object: &AtlasPrivateEndpoint{ + Spec: AtlasPrivateEndpointSpec{ + ExternalProject: &ExternalProjectReference{ + ID: "my-project-id", + }, + }, + }, + expectedErrors: []string{ + "spec: Invalid value: \"object\": must define a local connection secret when referencing an external project", + }, + }, + "kubernetes project references is set": { + object: &AtlasPrivateEndpoint{ + Spec: AtlasPrivateEndpointSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + }, + }, + }, + } + + assertCELValidation(t, "../../../config/crd/bases/atlas.mongodb.com_atlasprivateendpoints.yaml", tests) +} + +func TestPrivateEndpointExternalProjectReferenceConnectionSecret(t *testing.T) { + tests := celTestCase{ + "external project references is set without connection secret": { + object: &AtlasPrivateEndpoint{ + Spec: AtlasPrivateEndpointSpec{ + ExternalProject: &ExternalProjectReference{ + ID: "my-project-id", + }, + }, + }, + expectedErrors: []string{ + "spec: Invalid value: \"object\": must define a local connection secret when referencing an external project", + }, + }, + "external project references is set with connection secret": { + object: &AtlasPrivateEndpoint{ + Spec: AtlasPrivateEndpointSpec{ + ExternalProject: &ExternalProjectReference{ + ID: "my-project-id", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "my-dbuser-connection-secret", + }, + }, + }, + }, + }, + "kubernetes project references is set without connection secret": { + object: &AtlasPrivateEndpoint{ + Spec: AtlasPrivateEndpointSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + }, + }, + }, + "kubernetes project references is set with connection secret": { + object: &AtlasPrivateEndpoint{ + Spec: AtlasPrivateEndpointSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "my-dbuser-connection-secret", + }, + }, + }, + }, + }, + } + + assertCELValidation(t, "../../../config/crd/bases/atlas.mongodb.com_atlasprivateendpoints.yaml", tests) +} diff --git a/pkg/api/v1/project_reference_cel_test.go b/pkg/api/v1/project_reference_cel_test.go index 595cdd8648..2f2ee1b488 100644 --- a/pkg/api/v1/project_reference_cel_test.go +++ b/pkg/api/v1/project_reference_cel_test.go @@ -10,12 +10,13 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/cel" ) -type projectReferenceTestCase map[string]struct { +type celTestCase map[string]struct { object AtlasCustomResource + oldObject AtlasCustomResource expectedErrors []string } -func assertCELValidation(t *testing.T, crdPath string, tests projectReferenceTestCase) { +func assertCELValidation(t *testing.T, crdPath string, tests celTestCase) { t.Helper() validator, err := cel.VersionValidatorFromFile(t, crdPath, "v1") @@ -26,7 +27,10 @@ func assertCELValidation(t *testing.T, crdPath string, tests projectReferenceTes unstructuredObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tt.object) require.NoError(t, err) - errs := validator(unstructuredObject, nil) + unstructuredOldObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tt.oldObject) + require.NoError(t, err) + + errs := validator(unstructuredObject, unstructuredOldObject) require.Equal(t, len(tt.expectedErrors), len(errs)) diff --git a/pkg/api/v1/status/atlasprivateendpoint.go b/pkg/api/v1/status/atlasprivateendpoint.go new file mode 100644 index 0000000000..1b5c7a5960 --- /dev/null +++ b/pkg/api/v1/status/atlasprivateendpoint.go @@ -0,0 +1,64 @@ +/* +Copyright 2024 MongoDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package status + +import ( + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" +) + +// AtlasPrivateEndpointStatus is the most recent observed status of the AtlasPrivateEndpoint cluster. Read-only. +type AtlasPrivateEndpointStatus struct { + api.Common `json:",inline"` + // ServiceID is the unique identifier of the private endpoint service in Atlas + ServiceID string `json:"serviceId,omitempty"` + // ServiceStatus is the state of the private endpoint service + ServiceStatus string `json:"serviceStatus,omitempty"` + // Error is the description of the failure occurred when configuring the private endpoint + Error string `json:"error,omitempty"` + // ServiceName is the unique identifier of the Amazon Web Services (AWS) PrivateLink endpoint service or Azure Private Link Service managed by Atlas + ServiceName string `json:"serviceName,omitempty"` + // ResourceID is the root-relative path that identifies of the Atlas Azure Private Link Service + ResourceID string `json:"resourceId,omitempty"` + // ServiceAttachmentNames is the list of URLs that identifies endpoints that Atlas can use to access one service across the private connection + ServiceAttachmentNames []string `json:"serviceAttachmentNames,omitempty"` + // Endpoints are the status of the endpoints connected to the service + Endpoints []EndpointInterfaceStatus `json:"endpoints,omitempty"` +} + +// EndpointInterfaceStatus is the most recent observed status the interfaces attached to the configured service. Read-only. +type EndpointInterfaceStatus struct { + // ID is the external identifier set on the specification to configure the interface + ID string `json:"ID,omitempty"` + // ConnectionName is the label that Atlas generates that identifies the Azure private endpoint connection + ConnectionName string `json:"connectionName,omitempty"` + // GCPForwardingRules is the status of the customer GCP private endpoint(forwarding rules) + GCPForwardingRules []GCPForwardingRule `json:"gcpForwardingRules,omitempty"` + // InterfaceStatus is the state of the private endpoint interface + Status string `json:"InterfaceStatus,omitempty"` + // Error is the description of the failure occurred when configuring the private endpoint + Error string `json:"error,omitempty"` +} + +// GCPForwardingRule is the most recent observed status the GCP forwarding rules configured for an interface. Read-only. +type GCPForwardingRule struct { + Name string `json:"name,omitempty"` + Status string `json:"status,omitempty"` +} + +// +kubebuilder:object:generate=false + +type AtlasPrivateEndpointStatusOption func(s *AtlasPrivateEndpointStatus) diff --git a/pkg/api/v1/status/zz_generated.deepcopy.go b/pkg/api/v1/status/zz_generated.deepcopy.go index 41b2409e7b..827db36bea 100644 --- a/pkg/api/v1/status/zz_generated.deepcopy.go +++ b/pkg/api/v1/status/zz_generated.deepcopy.go @@ -160,6 +160,34 @@ func (in *AtlasNetworkPeer) DeepCopy() *AtlasNetworkPeer { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AtlasPrivateEndpointStatus) DeepCopyInto(out *AtlasPrivateEndpointStatus) { + *out = *in + in.Common.DeepCopyInto(&out.Common) + if in.ServiceAttachmentNames != nil { + in, out := &in.ServiceAttachmentNames, &out.ServiceAttachmentNames + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]EndpointInterfaceStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AtlasPrivateEndpointStatus. +func (in *AtlasPrivateEndpointStatus) DeepCopy() *AtlasPrivateEndpointStatus { + if in == nil { + return nil + } + out := new(AtlasPrivateEndpointStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AtlasProjectStatus) DeepCopyInto(out *AtlasProjectStatus) { *out = *in @@ -488,6 +516,26 @@ func (in *Endpoint) DeepCopy() *Endpoint { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EndpointInterfaceStatus) DeepCopyInto(out *EndpointInterfaceStatus) { + *out = *in + if in.GCPForwardingRules != nil { + in, out := &in.GCPForwardingRules, &out.GCPForwardingRules + *out = make([]GCPForwardingRule, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointInterfaceStatus. +func (in *EndpointInterfaceStatus) DeepCopy() *EndpointInterfaceStatus { + if in == nil { + return nil + } + out := new(EndpointInterfaceStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FeatureUsage) DeepCopyInto(out *FeatureUsage) { *out = *in @@ -518,6 +566,21 @@ func (in *GCPEndpoint) DeepCopy() *GCPEndpoint { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPForwardingRule) DeepCopyInto(out *GCPForwardingRule) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPForwardingRule. +func (in *GCPForwardingRule) DeepCopy() *GCPForwardingRule { + if in == nil { + return nil + } + out := new(GCPForwardingRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedNamespace) DeepCopyInto(out *ManagedNamespace) { *out = *in diff --git a/pkg/api/v1/zz_generated.deepcopy.go b/pkg/api/v1/zz_generated.deepcopy.go index 657519c604..8c6963dc55 100644 --- a/pkg/api/v1/zz_generated.deepcopy.go +++ b/pkg/api/v1/zz_generated.deepcopy.go @@ -20,6 +20,21 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/project" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSPrivateEndpointConfiguration) DeepCopyInto(out *AWSPrivateEndpointConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSPrivateEndpointConfiguration. +func (in *AWSPrivateEndpointConfiguration) DeepCopy() *AWSPrivateEndpointConfiguration { + if in == nil { + return nil + } + out := new(AWSPrivateEndpointConfiguration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AWSProviderConfig) DeepCopyInto(out *AWSProviderConfig) { *out = *in @@ -938,6 +953,108 @@ func (in *AtlasOnDemandPolicy) DeepCopy() *AtlasOnDemandPolicy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AtlasPrivateEndpoint) DeepCopyInto(out *AtlasPrivateEndpoint) { + *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 AtlasPrivateEndpoint. +func (in *AtlasPrivateEndpoint) DeepCopy() *AtlasPrivateEndpoint { + if in == nil { + return nil + } + out := new(AtlasPrivateEndpoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AtlasPrivateEndpoint) 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 *AtlasPrivateEndpointList) DeepCopyInto(out *AtlasPrivateEndpointList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AtlasPrivateEndpoint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AtlasPrivateEndpointList. +func (in *AtlasPrivateEndpointList) DeepCopy() *AtlasPrivateEndpointList { + if in == nil { + return nil + } + out := new(AtlasPrivateEndpointList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AtlasPrivateEndpointList) 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 *AtlasPrivateEndpointSpec) DeepCopyInto(out *AtlasPrivateEndpointSpec) { + *out = *in + if in.Project != nil { + in, out := &in.Project, &out.Project + *out = new(common.ResourceRefNamespaced) + **out = **in + } + if in.ExternalProject != nil { + in, out := &in.ExternalProject, &out.ExternalProject + *out = new(ExternalProjectReference) + **out = **in + } + in.LocalCredentialHolder.DeepCopyInto(&out.LocalCredentialHolder) + if in.AWSConfiguration != nil { + in, out := &in.AWSConfiguration, &out.AWSConfiguration + *out = make([]AWSPrivateEndpointConfiguration, len(*in)) + copy(*out, *in) + } + if in.AzureConfiguration != nil { + in, out := &in.AzureConfiguration, &out.AzureConfiguration + *out = make([]AzurePrivateEndpointConfiguration, len(*in)) + copy(*out, *in) + } + if in.GCPConfiguration != nil { + in, out := &in.GCPConfiguration, &out.GCPConfiguration + *out = make([]GCPPrivateEndpointConfiguration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AtlasPrivateEndpointSpec. +func (in *AtlasPrivateEndpointSpec) DeepCopy() *AtlasPrivateEndpointSpec { + if in == nil { + return nil + } + out := new(AtlasPrivateEndpointSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AtlasProject) DeepCopyInto(out *AtlasProject) { *out = *in @@ -1538,6 +1655,21 @@ func (in *AzureKeyVault) DeepCopy() *AzureKeyVault { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzurePrivateEndpointConfiguration) DeepCopyInto(out *AzurePrivateEndpointConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzurePrivateEndpointConfiguration. +func (in *AzurePrivateEndpointConfiguration) DeepCopy() *AzurePrivateEndpointConfiguration { + if in == nil { + return nil + } + out := new(AzurePrivateEndpointConfiguration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BiConnector) DeepCopyInto(out *BiConnector) { *out = *in @@ -2061,6 +2193,41 @@ func (in GCPEndpoints) DeepCopy() GCPEndpoints { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPPrivateEndpoint) DeepCopyInto(out *GCPPrivateEndpoint) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPPrivateEndpoint. +func (in *GCPPrivateEndpoint) DeepCopy() *GCPPrivateEndpoint { + if in == nil { + return nil + } + out := new(GCPPrivateEndpoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPPrivateEndpointConfiguration) DeepCopyInto(out *GCPPrivateEndpointConfiguration) { + *out = *in + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]GCPPrivateEndpoint, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPPrivateEndpointConfiguration. +func (in *GCPPrivateEndpointConfiguration) DeepCopy() *GCPPrivateEndpointConfiguration { + if in == nil { + return nil + } + out := new(GCPPrivateEndpointConfiguration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GoogleCloudKms) DeepCopyInto(out *GoogleCloudKms) { *out = *in