diff --git a/.golangci.yml b/.golangci.yml index fec8b28ce0..b919c63dd8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -76,6 +76,9 @@ issues: # EXC0011 stylecheck: Annoying issue about not having a comment. The rare codebase has such comments # - (comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form) + # ECX0012 staticcheck: Temporary disable deprecated LocalCredentialHolder and api.CredentialsProvider + - (api.LocalCredentialHolder is deprecated|api.CredentialsProvider is deprecated) + - at least one file in a package should have a package comment exclude-rules: diff --git a/PROJECT b/PROJECT index 3097a14aee..460234c863 100644 --- a/PROJECT +++ b/PROJECT @@ -104,4 +104,13 @@ resources: kind: AtlasPrivateEndpoint path: github.com/mongodb/mongodb-atlas-kubernetes/api/v1 version: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: mongodb.com + group: atlas + kind: AtlasNetworkPeering + path: github.com/mongodb/mongodb-atlas-kubernetes/api/v1 + version: v1 version: "3" diff --git a/config/crd/bases/atlas.mongodb.com_atlasnetworkpeerings.yaml b/config/crd/bases/atlas.mongodb.com_atlasnetworkpeerings.yaml new file mode 100644 index 0000000000..d9b7f350ae --- /dev/null +++ b/config/crd/bases/atlas.mongodb.com_atlasnetworkpeerings.yaml @@ -0,0 +1,214 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: atlasnetworkpeerings.atlas.mongodb.com +spec: + group: atlas.mongodb.com + names: + categories: + - atlas + kind: AtlasNetworkPeering + listKind: AtlasNetworkPeeringList + plural: atlasnetworkpeerings + shortNames: + - anp + singular: atlasnetworkpeering + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .spec.providerName + name: Provider + type: string + - jsonPath: .spec.containerId + name: Container ID + type: string + - jsonPath: .spec.projectIDRef.id + name: Project ID + type: string + name: v1 + schema: + openAPIV3Schema: + description: AtlasNetworkPeering is the Schema for the AtlasNetworkPeering + 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: AtlasNetworkPeeringSpec defines the desired state of AtlasNetworkPeering + properties: + awsConfiguration: + description: AWSConfiguration is the specific AWS settings for network + peering + properties: + accepterRegionName: + description: AccepterRegionName is the provider region name of + user's vpc. + type: string + awsAccountId: + description: AccountID of the user's vpc. + type: string + routeTableCidrBlock: + description: User VPC CIDR. + type: string + vpcId: + description: AWS VPC ID. + type: string + required: + - accepterRegionName + type: object + azureConfiguration: + description: AzureConfiguration is the specific Azure settings for + network peering + properties: + azureDirectoryId: + description: AzureDirectoryID is the unique identifier for an + Azure AD directory. + type: string + azureSubscriptionId: + description: AzureSubscriptionID is the unique identifier of the + Azure subscription in which the VNet resides. + type: string + resourceGroupName: + description: ResourceGroupName is the name of your Azure resource + group. + type: string + vnetName: + description: VNetName is name of your Azure VNet. Its applicable + only for Azure. + type: string + type: object + 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 + containerId: + description: ID of the network peer container. If not set, operator + will create a new container with ContainerRegion and AtlasCIDRBlock + input. + type: string + externalProjectRef: + description: ExternalProjectRef 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 network peering + properties: + gcpProjectId: + description: User GCP Project ID. Its applicable only for GCP. + type: string + networkName: + description: GCP Network Peer Name. Its applicable only for GCP. + type: string + type: object + 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 + providerName: + description: ProviderName is the name of the provider. If not set, + it will be set to "AWS". + type: string + 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: |- + AtlasNetworkPeeringStatus is a status for the AtlasNetworkPeering Custom resource. + Not the one included in the AtlasProject + 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 + 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 + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/atlas.mongodb.com_atlasprivateendpoints.yaml b/config/crd/bases/atlas.mongodb.com_atlasprivateendpoints.yaml index 66861afa19..b5a829d6ab 100644 --- a/config/crd/bases/atlas.mongodb.com_atlasprivateendpoints.yaml +++ b/config/crd/bases/atlas.mongodb.com_atlasprivateendpoints.yaml @@ -106,8 +106,8 @@ spec: - name type: object externalProjectRef: - description: ExternalProject holds the Atlas project ID the user belongs - to + description: ExternalProjectRef holds the Atlas project ID the user + belongs to properties: id: description: ID is the Atlas project ID diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 24a3953fbe..d9b1c67227 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -15,5 +15,6 @@ resources: - bases/atlas.mongodb.com_atlassearchindexconfigs.yaml - bases/atlas.mongodb.com_atlasbackupcompliancepolicies.yaml - bases/atlas.mongodb.com_atlasprivateendpoints.yaml + - bases/atlas.mongodb.com_atlasnetworkpeerings.yaml configurations: - kustomizeconfig.yaml diff --git a/config/crd/patches/cainjection_in_atlasnetworkpeerings.yaml b/config/crd/patches/cainjection_in_atlasnetworkpeerings.yaml new file mode 100644 index 0000000000..9a493f00d2 --- /dev/null +++ b/config/crd/patches/cainjection_in_atlasnetworkpeerings.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: atlasnetworkpeerings.atlas.mongodb.com diff --git a/config/crd/patches/webhook_in_atlasnetworkpeerings.yaml b/config/crd/patches/webhook_in_atlasnetworkpeerings.yaml new file mode 100644 index 0000000000..38a0a09118 --- /dev/null +++ b/config/crd/patches/webhook_in_atlasnetworkpeerings.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: atlasnetworkpeerings.atlas.mongodb.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/atlasnetworkpeering_editor_role.yaml b/config/rbac/atlasnetworkpeering_editor_role.yaml new file mode 100644 index 0000000000..9b058882d5 --- /dev/null +++ b/config/rbac/atlasnetworkpeering_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit atlasnetworkpeerings. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: atlasnetworkpeering-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: ako-scaffolding + app.kubernetes.io/part-of: ako-scaffolding + app.kubernetes.io/managed-by: kustomize + name: atlasnetworkpeering-editor-role +rules: +- apiGroups: + - atlas.mongodb.com + resources: + - atlasnetworkpeerings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - atlas.mongodb.com + resources: + - atlasnetworkpeerings/status + verbs: + - get diff --git a/config/rbac/atlasnetworkpeering_viewer_role.yaml b/config/rbac/atlasnetworkpeering_viewer_role.yaml new file mode 100644 index 0000000000..0bacac7296 --- /dev/null +++ b/config/rbac/atlasnetworkpeering_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view atlasnetworkpeerings. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: atlasnetworkpeering-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: ako-scaffolding + app.kubernetes.io/part-of: ako-scaffolding + app.kubernetes.io/managed-by: kustomize + name: atlasnetworkpeering-viewer-role +rules: +- apiGroups: + - atlas.mongodb.com + resources: + - atlasnetworkpeerings + verbs: + - get + - list + - watch +- apiGroups: + - atlas.mongodb.com + resources: + - atlasnetworkpeerings/status + verbs: + - get diff --git a/config/rbac/clusterwide/role.yaml b/config/rbac/clusterwide/role.yaml index 972ad91984..b3bbbd4ae3 100644 --- a/config/rbac/clusterwide/role.yaml +++ b/config/rbac/clusterwide/role.yaml @@ -33,6 +33,7 @@ rules: - atlasdatafederations - atlasdeployments - atlasfederatedauths + - atlasnetworkpeerings - atlasprojects - atlassearchindexconfigs - atlasstreamconnections @@ -56,6 +57,7 @@ rules: - atlasdatafederations/status - atlasdeployments/status - atlasfederatedauths/status + - atlasnetworkpeerings/status - atlasprojects/status - atlassearchindexconfigs/status - atlasstreamconnections/status @@ -65,3 +67,9 @@ rules: - get - patch - update +- apiGroups: + - atlas.mongodb.com + resources: + - atlasnetworkpeerings/finalizers + verbs: + - update diff --git a/config/samples/atlas_v1_atlasnetworkpeering.yaml b/config/samples/atlas_v1_atlasnetworkpeering.yaml new file mode 100644 index 0000000000..b6f7ec8c85 --- /dev/null +++ b/config/samples/atlas_v1_atlasnetworkpeering.yaml @@ -0,0 +1,6 @@ +apiVersion: atlas.mongodb.com/v1 +kind: AtlasNetworkPeering +metadata: + name: atlasnetworkpeering-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 2dcc484e99..99aa8ee4ea 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -8,4 +8,5 @@ resources: - atlas_v1_atlasbackuppolicy.yaml - atlas_v1_atlasbackupschedule.yaml - atlas_v1_atlasteam.yaml + - atlas_v1_atlasnetworkpeering.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/pkg/api/credentials.go b/pkg/api/credentials.go index 763d98c62a..23249f0ca5 100644 --- a/pkg/api/credentials.go +++ b/pkg/api/credentials.go @@ -5,6 +5,8 @@ type LocalRef string // +k8s:deepcopy-gen=false // CredentialsProvider gives access to custom local credentials +// +// Deprecated: CredentialsProvider is not needed when using ProjectReferences type CredentialsProvider interface { Credentials() *LocalObjectReference } @@ -19,6 +21,12 @@ type ResourceWithCredentials interface { } // LocalCredentialHolder is to be embedded by Specs of CRDs using custom local credentials +// +// Deprecated: LocalCredentialHolder has been replaced by ProjectReferences +// which embeds together the credentials and both the external k8s references +// to a project. That way, common logic can be used to resolve such references +// and the consumer types avoid the need to implement the CredentialsProvider +// interface type LocalCredentialHolder struct { ConnectionSecret *LocalObjectReference `json:"connectionSecret,omitempty"` } diff --git a/pkg/api/localref.go b/pkg/api/localref.go deleted file mode 100644 index 69af4ffd19..0000000000 --- a/pkg/api/localref.go +++ /dev/null @@ -1,8 +0,0 @@ -package api - -// LocalObjectReference is a reference to an object in the same namespace as the referent -type LocalObjectReference struct { - // Name of the resource being referred to - // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - Name string `json:"name"` -} diff --git a/pkg/api/refs.go b/pkg/api/refs.go new file mode 100644 index 0000000000..79b6af7bc6 --- /dev/null +++ b/pkg/api/refs.go @@ -0,0 +1,33 @@ +package api + +// LocalObjectReference is a reference to an object in the same namespace as the referent +type LocalObjectReference struct { + // Name of the resource being referred to + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + Name string `json:"name"` +} + +type ExternalProjectReference struct { + // ID is the Atlas project ID + // +kubebuilder:validation:Required + ID string `json:"id"` +} + +// ResourceRefNamespaced is a reference to a Kubernetes Resource that allows to configure the namespace +type ResourceRefNamespaced struct { + // Name is the name of the Kubernetes Resource + Name string `json:"name"` + + // Namespace is the namespace of the Kubernetes Resource + // +optional + Namespace string `json:"namespace"` +} + +type ProjectReferences struct { + ConnectionSecret *LocalObjectReference `json:"connectionSecret,omitempty"` + + // Project is a reference to AtlasProject resource the user belongs to + Project *ResourceRefNamespaced `json:"projectRef,omitempty"` + // ExternalProjectRef holds the Atlas project ID the user belongs to + ExternalProjectRef *ExternalProjectReference `json:"externalProjectRef,omitempty"` +} diff --git a/pkg/api/v1/atlasnetworkpeering_types.go b/pkg/api/v1/atlasnetworkpeering_types.go index a82016ea92..cd16dc5add 100644 --- a/pkg/api/v1/atlasnetworkpeering_types.go +++ b/pkg/api/v1/atlasnetworkpeering_types.go @@ -1,5 +1,5 @@ /* -Copyright 2020 MongoDB. +Copyright 2024. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,10 +16,56 @@ limitations under the License. package v1 -import "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/provider" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// +k8s:deepcopy-gen=false + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/provider" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status" +) +func init() { + SchemeBuilder.Register(&AtlasNetworkPeering{}, &AtlasNetworkPeeringList{}) +} + +// AtlasNetworkPeering is the Schema for the AtlasNetworkPeering API +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` +// +kubebuilder:printcolumn:name="Provider",type=string,JSONPath=`.spec.providerName` +// +kubebuilder:printcolumn:name="Container ID",type=string,JSONPath=`.spec.containerId` +// +kubebuilder:printcolumn:name="Project ID",type=string,JSONPath=`.spec.projectIDRef.id` +// +kubebuilder:subresource:status +// +groupName:=atlas.mongodb.com +// +kubebuilder:resource:categories=atlas,shortName=anp +type AtlasNetworkPeering struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AtlasNetworkPeeringSpec `json:"spec,omitempty"` + Status status.AtlasNetworkPeeringStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// AtlasNetworkPeeringList contains a list of AtlasNetworkPeering +type AtlasNetworkPeeringList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AtlasNetworkPeering `json:"items"` +} + +// +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" + +// AtlasNetworkPeeringSpec defines the desired state of AtlasNetworkPeering +type AtlasNetworkPeeringSpec struct { + api.ProjectReferences `json:",inline"` + + AtlasNetworkPeeringConfig `json:",inline"` +} + +// AtlasNetworkPeeringConfig defines the Atlas specifics of the desired state of AtlasNetworkPeering type AtlasNetworkPeeringConfig struct { //ProviderName is the name of the provider. If not set, it will be set to "AWS". // +optional @@ -39,8 +85,7 @@ type AtlasNetworkPeeringConfig struct { GCPConfiguration *GCPNetworkPeeringConfiguration `json:"gcpConfiguration,omitempty"` } -// +k8s:deepcopy-gen=false - +// AWSNetworkPeeringConfiguration defines tha Atlas desired state for AWS type AWSNetworkPeeringConfiguration struct { //AccepterRegionName is the provider region name of user's vpc. AccepterRegionName string `json:"accepterRegionName"` @@ -52,8 +97,7 @@ type AWSNetworkPeeringConfiguration struct { VpcID string `json:"vpcId,omitempty"` } -// +k8s:deepcopy-gen=false - +// AzureNetworkPeeringConfiguration defines tha Atlas desired state for Azure type AzureNetworkPeeringConfiguration struct { //AzureDirectoryID is the unique identifier for an Azure AD directory. AzureDirectoryID string `json:"azureDirectoryId,omitempty"` @@ -65,11 +109,19 @@ type AzureNetworkPeeringConfiguration struct { VNetName string `json:"vnetName,omitempty"` } -// +k8s:deepcopy-gen=false - +// GCPNetworkPeeringConfiguration defines tha Atlas desired state for Google type GCPNetworkPeeringConfiguration struct { // User GCP Project ID. Its applicable only for GCP. GCPProjectID string `json:"gcpProjectId,omitempty"` // GCP Network Peer Name. Its applicable only for GCP. NetworkName string `json:"networkName,omitempty"` } + +func (np *AtlasNetworkPeering) GetStatus() api.Status { + return np.Status +} + +func (np *AtlasNetworkPeering) UpdateStatus(conditions []api.Condition, options ...api.Option) { + np.Status.Conditions = conditions + np.Status.ObservedGeneration = np.ObjectMeta.Generation +} diff --git a/pkg/api/v1/atlasnetworkpeering_types_test.go b/pkg/api/v1/atlasnetworkpeering_types_test.go new file mode 100644 index 0000000000..6e2cd897f6 --- /dev/null +++ b/pkg/api/v1/atlasnetworkpeering_types_test.go @@ -0,0 +1,123 @@ +package v1 // nolint: dupl + +import ( + "testing" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" +) + +func TestNetworkPeeringProjectReference(t *testing.T) { + tests := celTestCase{ + "no project reference is set": { + object: &AtlasNetworkPeering{ + Spec: AtlasNetworkPeeringSpec{}, + }, + expectedErrors: []string{"spec: Invalid value: \"object\": must define only one project reference through externalProjectRef or projectRef"}, + }, + "both project references are set": { + object: &AtlasNetworkPeering{ + Spec: AtlasNetworkPeeringSpec{ + ProjectReferences: api.ProjectReferences{ + Project: &api.ResourceRefNamespaced{ + Name: "my-project", + }, + ExternalProjectRef: &api.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: &AtlasNetworkPeering{ + Spec: AtlasNetworkPeeringSpec{ + ProjectReferences: api.ProjectReferences{ + ExternalProjectRef: &api.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: &AtlasNetworkPeering{ + Spec: AtlasNetworkPeeringSpec{ + ProjectReferences: api.ProjectReferences{ + Project: &api.ResourceRefNamespaced{ + Name: "my-project", + }, + }, + }, + }, + }, + } + + assertCELValidation(t, "../../../config/crd/bases/atlas.mongodb.com_atlasnetworkpeerings.yaml", tests) +} + +func TestNetworkPeeringProjectReferenceConnectionSecret(t *testing.T) { + tests := celTestCase{ + "external project references is set without connection secret": { + object: &AtlasNetworkPeering{ + Spec: AtlasNetworkPeeringSpec{ + ProjectReferences: api.ProjectReferences{ + ExternalProjectRef: &api.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: &AtlasNetworkPeering{ + Spec: AtlasNetworkPeeringSpec{ + ProjectReferences: api.ProjectReferences{ + ExternalProjectRef: &api.ExternalProjectReference{ + ID: "my-project-id", + }, + ConnectionSecret: &api.LocalObjectReference{ + Name: "my-dbuser-connection-secret", + }, + }, + }, + }, + }, + "kubernetes project references is set without connection secret": { + object: &AtlasNetworkPeering{ + Spec: AtlasNetworkPeeringSpec{ + ProjectReferences: api.ProjectReferences{ + Project: &api.ResourceRefNamespaced{ + Name: "my-project", + }, + }, + }, + }, + }, + "kubernetes project references is set with connection secret": { + object: &AtlasNetworkPeering{ + Spec: AtlasNetworkPeeringSpec{ + ProjectReferences: api.ProjectReferences{ + Project: &api.ResourceRefNamespaced{ + Name: "my-project", + }, + ConnectionSecret: &api.LocalObjectReference{ + Name: "my-dbuser-connection-secret", + }, + }, + }, + }, + }, + } + + assertCELValidation(t, "../../../config/crd/bases/atlas.mongodb.com_atlasnetworkpeerings.yaml", tests) +} diff --git a/pkg/api/v1/atlasprivateendpoint_types.go b/pkg/api/v1/atlasprivateendpoint_types.go index 4d66426c5d..49783a10f6 100644 --- a/pkg/api/v1/atlasprivateendpoint_types.go +++ b/pkg/api/v1/atlasprivateendpoint_types.go @@ -38,9 +38,9 @@ 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 + // ExternalProjectRef holds the Atlas project ID the user belongs to // +kubebuilder:validation:Optional - ExternalProject *ExternalProjectReference `json:"externalProjectRef,omitempty"` + ExternalProjectRef *ExternalProjectReference `json:"externalProjectRef,omitempty"` // Local credentials api.LocalCredentialHolder `json:",inline"` diff --git a/pkg/api/v1/atlasprivateendpoint_types_test.go b/pkg/api/v1/atlasprivateendpoint_types_test.go index d774a04daf..e11f88adc0 100644 --- a/pkg/api/v1/atlasprivateendpoint_types_test.go +++ b/pkg/api/v1/atlasprivateendpoint_types_test.go @@ -21,7 +21,7 @@ func TestPrivateEndpointProjectReference(t *testing.T) { Project: &common.ResourceRefNamespaced{ Name: "my-project", }, - ExternalProject: &ExternalProjectReference{ + ExternalProjectRef: &ExternalProjectReference{ ID: "my-project-id", }, }, @@ -34,7 +34,7 @@ func TestPrivateEndpointProjectReference(t *testing.T) { "external project references is set": { object: &AtlasPrivateEndpoint{ Spec: AtlasPrivateEndpointSpec{ - ExternalProject: &ExternalProjectReference{ + ExternalProjectRef: &ExternalProjectReference{ ID: "my-project-id", }, }, @@ -62,7 +62,7 @@ func TestPrivateEndpointExternalProjectReferenceConnectionSecret(t *testing.T) { "external project references is set without connection secret": { object: &AtlasPrivateEndpoint{ Spec: AtlasPrivateEndpointSpec{ - ExternalProject: &ExternalProjectReference{ + ExternalProjectRef: &ExternalProjectReference{ ID: "my-project-id", }, }, @@ -74,7 +74,7 @@ func TestPrivateEndpointExternalProjectReferenceConnectionSecret(t *testing.T) { "external project references is set with connection secret": { object: &AtlasPrivateEndpoint{ Spec: AtlasPrivateEndpointSpec{ - ExternalProject: &ExternalProjectReference{ + ExternalProjectRef: &ExternalProjectReference{ ID: "my-project-id", }, LocalCredentialHolder: api.LocalCredentialHolder{ diff --git a/pkg/api/v1/status/atlasnetworkpeering.go b/pkg/api/v1/status/atlasnetworkpeering.go new file mode 100644 index 0000000000..44400b02a2 --- /dev/null +++ b/pkg/api/v1/status/atlasnetworkpeering.go @@ -0,0 +1,9 @@ +package status + +import "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" + +// AtlasNetworkPeeringStatus is a status for the AtlasNetworkPeering Custom resource. +// Not the one included in the AtlasProject +type AtlasNetworkPeeringStatus struct { + api.Common `json:",inline"` +} diff --git a/pkg/api/v1/status/zz_generated.deepcopy.go b/pkg/api/v1/status/zz_generated.deepcopy.go index 827db36bea..977b8a3d8c 100644 --- a/pkg/api/v1/status/zz_generated.deepcopy.go +++ b/pkg/api/v1/status/zz_generated.deepcopy.go @@ -160,6 +160,22 @@ 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 *AtlasNetworkPeeringStatus) DeepCopyInto(out *AtlasNetworkPeeringStatus) { + *out = *in + in.Common.DeepCopyInto(&out.Common) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AtlasNetworkPeeringStatus. +func (in *AtlasNetworkPeeringStatus) DeepCopy() *AtlasNetworkPeeringStatus { + if in == nil { + return nil + } + out := new(AtlasNetworkPeeringStatus) + in.DeepCopyInto(out) + 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 diff --git a/pkg/api/v1/zz_generated.deepcopy.go b/pkg/api/v1/zz_generated.deepcopy.go index 8c6963dc55..44925b70c4 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 *AWSNetworkPeeringConfiguration) DeepCopyInto(out *AWSNetworkPeeringConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSNetworkPeeringConfiguration. +func (in *AWSNetworkPeeringConfiguration) DeepCopy() *AWSNetworkPeeringConfiguration { + if in == nil { + return nil + } + out := new(AWSNetworkPeeringConfiguration) + in.DeepCopyInto(out) + return out +} + // 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 @@ -938,6 +953,112 @@ func (in *AtlasFederatedAuthSpec) DeepCopy() *AtlasFederatedAuthSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AtlasNetworkPeering) DeepCopyInto(out *AtlasNetworkPeering) { + *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 AtlasNetworkPeering. +func (in *AtlasNetworkPeering) DeepCopy() *AtlasNetworkPeering { + if in == nil { + return nil + } + out := new(AtlasNetworkPeering) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AtlasNetworkPeering) 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 *AtlasNetworkPeeringConfig) DeepCopyInto(out *AtlasNetworkPeeringConfig) { + *out = *in + if in.AWSConfiguration != nil { + in, out := &in.AWSConfiguration, &out.AWSConfiguration + *out = new(AWSNetworkPeeringConfiguration) + **out = **in + } + if in.AzureConfiguration != nil { + in, out := &in.AzureConfiguration, &out.AzureConfiguration + *out = new(AzureNetworkPeeringConfiguration) + **out = **in + } + if in.GCPConfiguration != nil { + in, out := &in.GCPConfiguration, &out.GCPConfiguration + *out = new(GCPNetworkPeeringConfiguration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AtlasNetworkPeeringConfig. +func (in *AtlasNetworkPeeringConfig) DeepCopy() *AtlasNetworkPeeringConfig { + if in == nil { + return nil + } + out := new(AtlasNetworkPeeringConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AtlasNetworkPeeringList) DeepCopyInto(out *AtlasNetworkPeeringList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AtlasNetworkPeering, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AtlasNetworkPeeringList. +func (in *AtlasNetworkPeeringList) DeepCopy() *AtlasNetworkPeeringList { + if in == nil { + return nil + } + out := new(AtlasNetworkPeeringList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AtlasNetworkPeeringList) 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 *AtlasNetworkPeeringSpec) DeepCopyInto(out *AtlasNetworkPeeringSpec) { + *out = *in + in.ProjectReferences.DeepCopyInto(&out.ProjectReferences) + in.AtlasNetworkPeeringConfig.DeepCopyInto(&out.AtlasNetworkPeeringConfig) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AtlasNetworkPeeringSpec. +func (in *AtlasNetworkPeeringSpec) DeepCopy() *AtlasNetworkPeeringSpec { + if in == nil { + return nil + } + out := new(AtlasNetworkPeeringSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AtlasOnDemandPolicy) DeepCopyInto(out *AtlasOnDemandPolicy) { *out = *in @@ -1020,8 +1141,8 @@ func (in *AtlasPrivateEndpointSpec) DeepCopyInto(out *AtlasPrivateEndpointSpec) *out = new(common.ResourceRefNamespaced) **out = **in } - if in.ExternalProject != nil { - in, out := &in.ExternalProject, &out.ExternalProject + if in.ExternalProjectRef != nil { + in, out := &in.ExternalProjectRef, &out.ExternalProjectRef *out = new(ExternalProjectReference) **out = **in } @@ -1655,6 +1776,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 *AzureNetworkPeeringConfiguration) DeepCopyInto(out *AzureNetworkPeeringConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureNetworkPeeringConfiguration. +func (in *AzureNetworkPeeringConfiguration) DeepCopy() *AzureNetworkPeeringConfiguration { + if in == nil { + return nil + } + out := new(AzureNetworkPeeringConfiguration) + in.DeepCopyInto(out) + 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 @@ -2193,6 +2329,21 @@ 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 *GCPNetworkPeeringConfiguration) DeepCopyInto(out *GCPNetworkPeeringConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPNetworkPeeringConfiguration. +func (in *GCPNetworkPeeringConfiguration) DeepCopy() *GCPNetworkPeeringConfiguration { + if in == nil { + return nil + } + out := new(GCPNetworkPeeringConfiguration) + in.DeepCopyInto(out) + 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 diff --git a/pkg/api/zz_generated.deepcopy.go b/pkg/api/zz_generated.deepcopy.go index 3e2216c836..3931b34022 100644 --- a/pkg/api/zz_generated.deepcopy.go +++ b/pkg/api/zz_generated.deepcopy.go @@ -50,6 +50,21 @@ func (in *Condition) DeepCopy() *Condition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalProjectReference) DeepCopyInto(out *ExternalProjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalProjectReference. +func (in *ExternalProjectReference) DeepCopy() *ExternalProjectReference { + if in == nil { + return nil + } + out := new(ExternalProjectReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LocalCredentialHolder) DeepCopyInto(out *LocalCredentialHolder) { *out = *in @@ -84,3 +99,48 @@ func (in *LocalObjectReference) DeepCopy() *LocalObjectReference { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectReferences) DeepCopyInto(out *ProjectReferences) { + *out = *in + if in.ConnectionSecret != nil { + in, out := &in.ConnectionSecret, &out.ConnectionSecret + *out = new(LocalObjectReference) + **out = **in + } + if in.Project != nil { + in, out := &in.Project, &out.Project + *out = new(ResourceRefNamespaced) + **out = **in + } + if in.ExternalProjectRef != nil { + in, out := &in.ExternalProjectRef, &out.ExternalProjectRef + *out = new(ExternalProjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectReferences. +func (in *ProjectReferences) DeepCopy() *ProjectReferences { + if in == nil { + return nil + } + out := new(ProjectReferences) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceRefNamespaced) DeepCopyInto(out *ResourceRefNamespaced) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceRefNamespaced. +func (in *ResourceRefNamespaced) DeepCopy() *ResourceRefNamespaced { + if in == nil { + return nil + } + out := new(ResourceRefNamespaced) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/atlasnetworkpeering/atlasnetworkpeering_controller.go b/pkg/controller/atlasnetworkpeering/atlasnetworkpeering_controller.go new file mode 100644 index 0000000000..fc95425b6c --- /dev/null +++ b/pkg/controller/atlasnetworkpeering/atlasnetworkpeering_controller.go @@ -0,0 +1,143 @@ +// Package atlasnetworkpeering holds the network peering controller +package atlasnetworkpeering + +/* +Copyright 2024. + +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. +*/ + +import ( + "context" + "errors" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/networkpeering" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/customresource" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/reconciler" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/statushandler" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" +) + +const ( + typeName = "AtlasNetworkPeering" +) + +// AtlasNetworkPeeringReconciler reconciles a AtlasNetworkPeering object +type AtlasNetworkPeeringReconciler struct { + reconciler.Reconciler + Scheme *runtime.Scheme + EventRecorder record.EventRecorder +} + +type reconcileRequest struct { + workflowCtx *workflow.Context + service networkpeering.NetworkPeeringService + projectID string + networkPeering *akov2.AtlasNetworkPeering +} + +//+kubebuilder:rbac:groups=atlas.mongodb.com,resources=atlasnetworkpeerings,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=atlas.mongodb.com,resources=atlasnetworkpeerings/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=atlas.mongodb.com,resources=atlasnetworkpeerings/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the AtlasNetworkPeering object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile +func (r *AtlasNetworkPeeringReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Log.Infow("-> Starting AtlasNetworkPeering reconciliation") + + akoNetworkPeering := akov2.AtlasNetworkPeering{} + result := customresource.PrepareResource(ctx, r.Client, req, &akoNetworkPeering, r.Log) + if !result.IsOk() { + return result.ReconcileResult(), errors.New(result.GetMessage()) + } + + if customresource.ReconciliationShouldBeSkipped(&akoNetworkPeering) { + return r.Skip(ctx, typeName, &akoNetworkPeering, &akoNetworkPeering.Spec) + } + + conditions := api.InitCondition(&akoNetworkPeering, api.FalseCondition(api.ReadyType)) + workflowCtx := workflow.NewContext(r.Log, conditions, ctx) + defer statushandler.Update(workflowCtx, r.Client, r.EventRecorder, &akoNetworkPeering) + + isValid := customresource.ValidateResourceVersion(workflowCtx, &akoNetworkPeering, r.Log) + if !isValid.IsOk() { + return r.Invalidate(isValid) + } + + if !r.AtlasProvider.IsResourceSupported(&akoNetworkPeering) { + return r.Unsupport(workflowCtx, typeName) + } + + projectRefs := &akoNetworkPeering.Spec.ProjectReferences + credentials, err := r.SelectCredentials(ctx, projectRefs, &akoNetworkPeering) + if err != nil { + return r.terminate(workflowCtx, &akoNetworkPeering, api.ReadyType, workflow.AtlasAPIAccessNotConfigured, err) + } + sdkClient, _, err := r.AtlasProvider.SdkClient(ctx, credentials, r.Log) + if err != nil { + return r.terminate(workflowCtx, &akoNetworkPeering, api.ReadyType, workflow.AtlasAPIAccessNotConfigured, err) + } + projectID, err := r.GetProjectID(ctx, projectRefs, sdkClient, akoNetworkPeering.Namespace) + if err != nil { + return r.terminate(workflowCtx, &akoNetworkPeering, api.ReadyType, workflow.AtlasAPIAccessNotConfigured, err) + } + return r.handle(&reconcileRequest{ + workflowCtx: workflowCtx, + service: networkpeering.NewNetworkPeeringService(sdkClient.NetworkPeeringApi), + projectID: projectID, + networkPeering: &akoNetworkPeering, + }) +} + +func (r *AtlasNetworkPeeringReconciler) handle(req *reconcileRequest) (ctrl.Result, error) { + r.Log.Infow("handling network peering reconcile request", + "service set", (req.service != nil), "projectID", req.projectID, "networkPeering", req.networkPeering) + // TODO: state machine goes here + return ctrl.Result{}, nil +} + +func (r *AtlasNetworkPeeringReconciler) terminate( + ctx *workflow.Context, + resource api.AtlasCustomResource, + condition api.ConditionType, + reason workflow.ConditionReason, + err error, +) (ctrl.Result, error) { + r.Log.Errorf("resource %T(%s/%s) failed on condition %s: %s", + resource, resource.GetNamespace(), resource.GetName(), condition, err) + result := workflow.Terminate(reason, err.Error()) + ctx.SetConditionFalse(api.ReadyType). + SetConditionFromResult(condition, result) + + return result.ReconcileResult(), nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AtlasNetworkPeeringReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&akov2.AtlasNetworkPeering{}). + Complete(r) +} diff --git a/pkg/controller/reconciler/reconciler.go b/pkg/controller/reconciler/reconciler.go new file mode 100644 index 0000000000..eb222f2474 --- /dev/null +++ b/pkg/controller/reconciler/reconciler.go @@ -0,0 +1,103 @@ +package reconciler + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "go.mongodb.org/atlas-sdk/v20231115008/admin" + "go.uber.org/zap" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/kube" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/project" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/customresource" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" +) + +type Reconciler struct { + client.Client + AtlasProvider atlas.Provider + Log *zap.SugaredLogger +} + +func (r *Reconciler) SelectCredentials(ctx context.Context, projectRefs *api.ProjectReferences, resource api.AtlasCustomResource) (*client.ObjectKey, error) { + if projectRefs.ExternalProjectRef != nil { + if projectRefs.ConnectionSecret == nil { + return nil, errors.New("externalProjectIDRef is set but connectionSecret is missing") + } + return &client.ObjectKey{Name: resource.GetName(), Namespace: resource.GetNamespace()}, nil + } + if projectRefs.Project != nil { + if projectRefs.ConnectionSecret != nil { + return &client.ObjectKey{Name: projectRefs.ConnectionSecret.Name, Namespace: resource.GetNamespace()}, nil + } + project := &akov2.AtlasProject{} + err := r.Client.Get(ctx, + client.ObjectKey{Name: projectRefs.Project.Name, Namespace: projectRefs.Project.Namespace}, project) + if err != nil { + return nil, fmt.Errorf("can not read AtlasProject %q from Kubernetes: %w", projectRefs.Project.Name, err) + } + return project.ConnectionSecretObjectKey(), nil + } + return nil, errors.New("either 'externalProjectIDRef' or 'projectRef' must be set for the AtlasCustomRole resource") +} + +func (r *Reconciler) GetProjectID(ctx context.Context, projectRefs *api.ProjectReferences, atlasClient *admin.APIClient, ns string) (string, error) { + if projectRefs.ExternalProjectRef != nil { + projectsService := project.NewProjectAPIService(atlasClient.ProjectsApi) + if _, err := projectsService.GetProject(ctx, projectRefs.ExternalProjectRef.ID); err != nil { + return "", fmt.Errorf("failed to verify Atlas Project from external reference: %w", err) + } + return projectRefs.ExternalProjectRef.ID, nil + } else { + atlasProject := &akov2.AtlasProject{} + if err := r.Client.Get(ctx, objectKey(projectRefs, ns), atlasProject); err != nil { + return "", fmt.Errorf("failed to get Project from Kubernetes: %w", err) + } + return atlasProject.ID(), nil + } +} + +func (r *Reconciler) Skip(ctx context.Context, typeName string, resource api.AtlasCustomResource, spec any) (ctrl.Result, error) { + msg := fmt.Sprintf("-> Skipping %s reconciliation as annotation %s=%s", + typeName, customresource.ReconciliationPolicyAnnotation, customresource.ReconciliationPolicySkip) + r.Log.Infow(msg, "spec", spec) + if !resource.GetDeletionTimestamp().IsZero() { + if err := customresource.ManageFinalizer(ctx, r.Client, resource, customresource.UnsetFinalizer); err != nil { + result := workflow.Terminate(workflow.Internal, err.Error()) + r.Log.Errorw("Failed to remove finalizer", "terminate", err) + + return result.ReconcileResult(), nil + } + } + + return workflow.OK().ReconcileResult(), nil +} + +func (r *Reconciler) Invalidate(invalid workflow.Result) (ctrl.Result, error) { + // note: ValidateResourceVersion already set the state so we don't have to do it here. + r.Log.Debugf("AtlasNetworkPeering is invalid: %v", invalid) + return invalid.ReconcileResult(), nil +} + +func (r *Reconciler) Unsupport(ctx *workflow.Context, typeName string) (ctrl.Result, error) { + unsupported := workflow.Terminate( + workflow.AtlasGovUnsupported, + fmt.Sprintf("the %s is not supported by Atlas for government", typeName), + ).WithoutRetry() + ctx.SetConditionFromResult(api.ReadyType, unsupported) + return unsupported.ReconcileResult(), nil +} + +func objectKey(projectRefs *api.ProjectReferences, fallbackNamespace string) client.ObjectKey { + ns := fallbackNamespace + if projectRefs.Project.Namespace != "" { + ns = projectRefs.Project.Namespace + } + return kube.ObjectKey(ns, projectRefs.Project.Name) +} diff --git a/test/helper/e2e/data/user.go b/test/helper/e2e/data/user.go index 6f7071990c..473f67a3ce 100644 --- a/test/helper/e2e/data/user.go +++ b/test/helper/e2e/data/user.go @@ -119,6 +119,7 @@ func WithExternalProjectRef(projectID, credentialsName string) func(user *akov2. user.Spec.ExternalProjectRef = &akov2.ExternalProjectReference{ ID: projectID, } + // lint:ignore SA1019 must replace api.LocalCredentialHolder with api.ProjectRefs in users user.Spec.LocalCredentialHolder = api.LocalCredentialHolder{ ConnectionSecret: &api.LocalObjectReference{ Name: credentialsName,