diff --git a/PROJECT b/PROJECT index 44fae8561..3397ad96b 100644 --- a/PROJECT +++ b/PROJECT @@ -150,4 +150,13 @@ resources: kind: AppSource path: github.com/splunk/splunk-operator/api/apps/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: splunk.com + group: apps + kind: App + path: github.com/splunk/splunk-operator/api/apps/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/apps/v1alpha1/app_types.go b/api/apps/v1alpha1/app_types.go new file mode 100644 index 000000000..4bc31a0e9 --- /dev/null +++ b/api/apps/v1alpha1/app_types.go @@ -0,0 +1,138 @@ +// Copyright (c) 2018-2026 Splunk Inc. All rights reserved. + +// 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // AppPausedAnnotation is the annotation that pauses the reconciliation (triggers + // an immediate requeue) + AppPausedAnnotation = "apps.splunk.com/paused" +) + +// AppTargetRef defines the target environment the app should bind to. +type AppTargetRef struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum="ClusterManager";"ClusterMaster";"IndexerCluster";"IngestorCluster";"LicenseManager";"LicenseMaster";"MonitoringConsole";"SearchHeadCluster";"Standalone" + Kind string `json:"kind"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` +} + +// AppPackageSpec defines the package location within the source. +type AppPackageSpec struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Path string `json:"path"` +} + +// AppSourceRef defines the app source details. +type AppSourceRef struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` +} + +// AppSpec defines the desired state of App. +type AppSpec struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + AppID string `json:"appID"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Version string `json:"version"` + + // +kubebuilder:validation:Required + TargetRef AppTargetRef `json:"targetRef"` + + // +kubebuilder:validation:Required + SourceRef AppSourceRef `json:"sourceRef"` + + // +kubebuilder:validation:Required + Package AppPackageSpec `json:"package"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum="local";"cluster" + Scope string `json:"scope"` +} + +// AppStatus defines the observed state of App. +type AppStatus struct { + // Phase of the app resource + Phase string `json:"phase,omitempty"` + + // Auxiliary message describing CR status + Message string `json:"message,omitempty"` + + // InstalledVersion is the app version installed on target + InstalledVersion string `json:"installedVersion,omitempty"` + + // Artifact tracks the resolved app package details + Artifact *AppArtifactStatus `json:"artifact,omitempty"` + + // ObservedGeneration tracks the latest reconciled generation + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions represent the latest available observations of the app + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// AppArtifactStatus defines resolved app artifact details. +type AppArtifactStatus struct { + // ResolvedPath is the resolved artifact path within the source + ResolvedPath string `json:"resolvedPath,omitempty"` + + // Etag is the artifact hash or ETag used for change detection + Etag string `json:"etag,omitempty"` + + // LastFetchedTime is the last time the artifact was fetched + LastFetchedTime metav1.Time `json:"lastFetchedTime,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// App is the Schema for the apps API. +// +k8s:openapi-gen=true +// +kubebuilder:resource:path=apps,scope=Namespaced +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Status of app" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age of app resource" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.message",description="Auxiliary message describing CR status" +// +kubebuilder:storageversion +type App struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` + + Spec AppSpec `json:"spec"` + Status AppStatus `json:"status,omitempty,omitzero"` +} + +// +kubebuilder:object:root=true + +// AppList contains a list of App. +type AppList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []App `json:"items"` +} + +func init() { + SchemeBuilder.Register(&App{}, &AppList{}) +} diff --git a/api/apps/v1alpha1/zz_generated.deepcopy.go b/api/apps/v1alpha1/zz_generated.deepcopy.go index ff42f4ccf..8e703fd48 100644 --- a/api/apps/v1alpha1/zz_generated.deepcopy.go +++ b/api/apps/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,96 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *App) DeepCopyInto(out *App) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new App. +func (in *App) DeepCopy() *App { + if in == nil { + return nil + } + out := new(App) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *App) 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 *AppArtifactStatus) DeepCopyInto(out *AppArtifactStatus) { + *out = *in + in.LastFetchedTime.DeepCopyInto(&out.LastFetchedTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppArtifactStatus. +func (in *AppArtifactStatus) DeepCopy() *AppArtifactStatus { + if in == nil { + return nil + } + out := new(AppArtifactStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppList) DeepCopyInto(out *AppList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]App, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppList. +func (in *AppList) DeepCopy() *AppList { + if in == nil { + return nil + } + out := new(AppList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AppList) 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 *AppPackageSpec) DeepCopyInto(out *AppPackageSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppPackageSpec. +func (in *AppPackageSpec) DeepCopy() *AppPackageSpec { + if in == nil { + return nil + } + out := new(AppPackageSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AppSource) DeepCopyInto(out *AppSource) { *out = *in @@ -115,6 +205,21 @@ func (in *AppSourceList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppSourceRef) DeepCopyInto(out *AppSourceRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppSourceRef. +func (in *AppSourceRef) DeepCopy() *AppSourceRef { + if in == nil { + return nil + } + out := new(AppSourceRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AppSourceS3Spec) DeepCopyInto(out *AppSourceS3Spec) { *out = *in @@ -194,6 +299,66 @@ func (in *AppSourceStatus) DeepCopy() *AppSourceStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppSpec) DeepCopyInto(out *AppSpec) { + *out = *in + out.TargetRef = in.TargetRef + out.SourceRef = in.SourceRef + out.Package = in.Package +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppSpec. +func (in *AppSpec) DeepCopy() *AppSpec { + if in == nil { + return nil + } + out := new(AppSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppStatus) DeepCopyInto(out *AppStatus) { + *out = *in + if in.Artifact != nil { + in, out := &in.Artifact, &out.Artifact + *out = new(AppArtifactStatus) + (*in).DeepCopyInto(*out) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppStatus. +func (in *AppStatus) DeepCopy() *AppStatus { + if in == nil { + return nil + } + out := new(AppStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppTargetRef) DeepCopyInto(out *AppTargetRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppTargetRef. +func (in *AppTargetRef) DeepCopy() *AppTargetRef { + if in == nil { + return nil + } + out := new(AppTargetRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DiscoveredApp) DeepCopyInto(out *DiscoveredApp) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 9e61b7276..468037185 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -326,6 +326,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "AppSource") os.Exit(1) } + if err := (&appscontroller.AppReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "App") + os.Exit(1) + } //+kubebuilder:scaffold:builder // Register certificate watchers with the manager diff --git a/config/crd/bases/apps.splunk.com_apps.yaml b/config/crd/bases/apps.splunk.com_apps.yaml new file mode 100644 index 000000000..fe148c898 --- /dev/null +++ b/config/crd/bases/apps.splunk.com_apps.yaml @@ -0,0 +1,214 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: apps.apps.splunk.com +spec: + group: apps.splunk.com + names: + kind: App + listKind: AppList + plural: apps + singular: app + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Status of app + jsonPath: .status.phase + name: Phase + type: string + - description: Age of app resource + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Auxiliary message describing CR status + jsonPath: .status.message + name: Message + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: App is the Schema for the apps 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: AppSpec defines the desired state of App. + properties: + appID: + minLength: 1 + type: string + package: + description: AppPackageSpec defines the package location within the + source. + properties: + path: + minLength: 1 + type: string + required: + - path + type: object + scope: + enum: + - local + - cluster + type: string + sourceRef: + description: AppSourceRef defines the app source details. + properties: + name: + minLength: 1 + type: string + required: + - name + type: object + targetRef: + description: AppTargetRef defines the target environment the app should + bind to. + properties: + kind: + enum: + - ClusterManager + - ClusterMaster + - IndexerCluster + - IngestorCluster + - LicenseManager + - LicenseMaster + - MonitoringConsole + - SearchHeadCluster + - Standalone + type: string + name: + minLength: 1 + type: string + required: + - kind + - name + type: object + version: + minLength: 1 + type: string + required: + - appID + - package + - scope + - sourceRef + - targetRef + - version + type: object + status: + description: AppStatus defines the observed state of App. + properties: + artifact: + description: Artifact tracks the resolved app package details + properties: + etag: + description: Etag is the artifact hash or ETag used for change + detection + type: string + lastFetchedTime: + description: LastFetchedTime is the last time the artifact was + fetched + format: date-time + type: string + resolvedPath: + description: ResolvedPath is the resolved artifact path within + the source + type: string + type: object + conditions: + description: Conditions represent the latest available observations + of the app + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the 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: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + installedVersion: + description: InstalledVersion is the app version installed on target + type: string + message: + description: Auxiliary message describing CR status + type: string + observedGeneration: + description: ObservedGeneration tracks the latest reconciled generation + format: int64 + type: integer + phase: + description: Phase of the app resource + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 79c5bfb92..b6882604b 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -14,6 +14,7 @@ resources: - bases/enterprise.splunk.com_queues.yaml - bases/enterprise.splunk.com_objectstorages.yaml - bases/apps.splunk.com_appsources.yaml +- bases/apps.splunk.com_apps.yaml #+kubebuilder:scaffold:crdkustomizeresource diff --git a/config/rbac/apps_app_admin_role.yaml b/config/rbac/apps_app_admin_role.yaml new file mode 100644 index 000000000..ffa48fda5 --- /dev/null +++ b/config/rbac/apps_app_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project splunk-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over apps.splunk.com. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: splunk-operator + app.kubernetes.io/managed-by: kustomize + name: apps-app-admin-role +rules: +- apiGroups: + - apps.splunk.com + resources: + - apps + verbs: + - '*' +- apiGroups: + - apps.splunk.com + resources: + - apps/status + verbs: + - get diff --git a/config/rbac/apps_app_editor_role.yaml b/config/rbac/apps_app_editor_role.yaml new file mode 100644 index 000000000..3f8a938d7 --- /dev/null +++ b/config/rbac/apps_app_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project splunk-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the apps.splunk.com. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: splunk-operator + app.kubernetes.io/managed-by: kustomize + name: apps-app-editor-role +rules: +- apiGroups: + - apps.splunk.com + resources: + - apps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.splunk.com + resources: + - apps/status + verbs: + - get diff --git a/config/rbac/apps_app_viewer_role.yaml b/config/rbac/apps_app_viewer_role.yaml new file mode 100644 index 000000000..a3162d17e --- /dev/null +++ b/config/rbac/apps_app_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project splunk-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to apps.splunk.com resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: splunk-operator + app.kubernetes.io/managed-by: kustomize + name: apps-app-viewer-role +rules: +- apiGroups: + - apps.splunk.com + resources: + - apps + verbs: + - get + - list + - watch +- apiGroups: + - apps.splunk.com + resources: + - apps/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 7a749f7e7..4ce205d4c 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -21,6 +21,9 @@ resources: # default, aiding admins in cluster management. Those roles are # not used by the splunk-operator itself. You can comment the following lines # if you do not want those helpers be installed with your Project. +- apps_app_admin_role.yaml +- apps_app_editor_role.yaml +- apps_app_viewer_role.yaml - apps_appsource_admin_role.yaml - apps_appsource_editor_role.yaml - apps_appsource_viewer_role.yaml diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 2e6aa754e..b70d057c0 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -47,6 +47,7 @@ rules: - apiGroups: - apps.splunk.com resources: + - apps - appsources verbs: - create @@ -59,12 +60,14 @@ rules: - apiGroups: - apps.splunk.com resources: + - apps/finalizers - appsources/finalizers verbs: - update - apiGroups: - apps.splunk.com resources: + - apps/status - appsources/status verbs: - get diff --git a/config/samples/apps_v1alpha1_app.yaml b/config/samples/apps_v1alpha1_app.yaml new file mode 100644 index 000000000..8d4db3abb --- /dev/null +++ b/config/samples/apps_v1alpha1_app.yaml @@ -0,0 +1,18 @@ +apiVersion: apps.splunk.com/v1alpha1 +kind: App +metadata: + labels: + app.kubernetes.io/name: splunk-operator + app.kubernetes.io/managed-by: kustomize + name: app-sample +spec: + appID: sample-app + version: "1.0.0" + targetRef: + kind: Standalone + name: standalone-sample + sourceRef: + name: appsource-sample-s3 + package: + path: apps/sample-app.tgz + scope: local diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index e55533bed..5caa932f4 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -17,4 +17,5 @@ resources: - enterprise_v4_queue.yaml - enterprise_v4_objectstorage.yaml - apps_v1alpha1_appsource.yaml +- apps_v1alpha1_app.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/internal/controller/apps/app_controller.go b/internal/controller/apps/app_controller.go new file mode 100644 index 000000000..e4c54e566 --- /dev/null +++ b/internal/controller/apps/app_controller.go @@ -0,0 +1,63 @@ +/* +Copyright 2021. + +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 apps + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + appsv1alpha1 "github.com/splunk/splunk-operator/api/apps/v1alpha1" +) + +// AppReconciler reconciles a App object +type AppReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=apps.splunk.com,resources=apps,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps.splunk.com,resources=apps/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=apps.splunk.com,resources=apps/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 App 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.21.0/pkg/reconcile +func (r *AppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = logf.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AppReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&appsv1alpha1.App{}). + Named("apps-app"). + Complete(r) +} diff --git a/internal/controller/apps/app_controller_test.go b/internal/controller/apps/app_controller_test.go new file mode 100644 index 000000000..e2f448500 --- /dev/null +++ b/internal/controller/apps/app_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2021. + +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 apps + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + appsv1alpha1 "github.com/splunk/splunk-operator/api/apps/v1alpha1" +) + +var _ = Describe("App Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + app := &appsv1alpha1.App{} + + BeforeEach(func() { + By("creating the custom resource for the Kind App") + err := k8sClient.Get(ctx, typeNamespacedName, app) + if err != nil && errors.IsNotFound(err) { + resource := &appsv1alpha1.App{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &appsv1alpha1.App{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance App") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &AppReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +})