diff --git a/PROJECT b/PROJECT index 3ac92d02..29ddb5eb 100644 --- a/PROJECT +++ b/PROJECT @@ -38,4 +38,13 @@ resources: kind: Ingress path: github.com/mirantis/boundless-operator/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: mirantis.com + group: boundless + kind: Manifest + path: github.com/mirantis/boundless-operator/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/addon_types.go b/api/v1alpha1/addon_types.go index 266db3d4..1bea06ec 100644 --- a/api/v1alpha1/addon_types.go +++ b/api/v1alpha1/addon_types.go @@ -13,14 +13,15 @@ type AddonSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - Name string `json:"name"` - Kind string `json:"kind"` - Enabled bool `json:"enabled"` - Namespace string `json:"namespace,omitempty"` - Chart Chart `json:"chart"` + Name string `json:"name"` + Kind string `json:"kind"` + Enabled bool `json:"enabled"` + Namespace string `json:"namespace,omitempty"` + Chart ChartInfo `json:"chart,omitempty"` + Manifest ManifestInfo `json:"manifest,omitempty"` } -type Chart struct { +type ChartInfo struct { Name string `json:"name"` Repo string `json:"repo"` Version string `json:"version"` @@ -28,6 +29,10 @@ type Chart struct { Values string `json:"values,omitempty"` } +type ManifestInfo struct { + URL string `json:"url"` +} + // AddonStatus defines the observed state of Addon type AddonStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster diff --git a/api/v1alpha1/manifest_types.go b/api/v1alpha1/manifest_types.go new file mode 100644 index 00000000..4053d8ad --- /dev/null +++ b/api/v1alpha1/manifest_types.go @@ -0,0 +1,48 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ManifestSpec defines the desired state of Manifest +type ManifestSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of Manifest. Edit manifest_types.go to remove/update + Url string `json:"url,omitempty"` +} + +// ManifestStatus defines the observed state of Manifest +type ManifestStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Manifest is the Schema for the manifests API +type Manifest struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ManifestSpec `json:"spec,omitempty"` + Status ManifestStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ManifestList contains a list of Manifest +type ManifestList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Manifest `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Manifest{}, &ManifestList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 373a7f75..afac6cdc 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -73,6 +73,7 @@ func (in *AddonList) DeepCopyObject() runtime.Object { func (in *AddonSpec) DeepCopyInto(out *AddonSpec) { *out = *in in.Chart.DeepCopyInto(&out.Chart) + out.Manifest = in.Manifest } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddonSpec. @@ -191,7 +192,7 @@ func (in *BlueprintStatus) DeepCopy() *BlueprintStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Chart) DeepCopyInto(out *Chart) { +func (in *ChartInfo) DeepCopyInto(out *ChartInfo) { *out = *in if in.Set != nil { in, out := &in.Set, &out.Set @@ -202,12 +203,12 @@ func (in *Chart) DeepCopyInto(out *Chart) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Chart. -func (in *Chart) DeepCopy() *Chart { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChartInfo. +func (in *ChartInfo) DeepCopy() *ChartInfo { if in == nil { return nil } - out := new(Chart) + out := new(ChartInfo) in.DeepCopyInto(out) return out } @@ -347,3 +348,107 @@ func (in *IngressStatus) DeepCopy() *IngressStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Manifest) DeepCopyInto(out *Manifest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Manifest. +func (in *Manifest) DeepCopy() *Manifest { + if in == nil { + return nil + } + out := new(Manifest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Manifest) 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 *ManifestInfo) DeepCopyInto(out *ManifestInfo) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestInfo. +func (in *ManifestInfo) DeepCopy() *ManifestInfo { + if in == nil { + return nil + } + out := new(ManifestInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManifestList) DeepCopyInto(out *ManifestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Manifest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestList. +func (in *ManifestList) DeepCopy() *ManifestList { + if in == nil { + return nil + } + out := new(ManifestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManifestList) 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 *ManifestSpec) DeepCopyInto(out *ManifestSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestSpec. +func (in *ManifestSpec) DeepCopy() *ManifestSpec { + if in == nil { + return nil + } + out := new(ManifestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManifestStatus) DeepCopyInto(out *ManifestStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestStatus. +func (in *ManifestStatus) DeepCopy() *ManifestStatus { + if in == nil { + return nil + } + out := new(ManifestStatus) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/boundless.mirantis.com_addons.yaml b/config/crd/bases/boundless.mirantis.com_addons.yaml index f5b070b8..68b357d1 100644 --- a/config/crd/bases/boundless.mirantis.com_addons.yaml +++ b/config/crd/bases/boundless.mirantis.com_addons.yaml @@ -61,12 +61,18 @@ spec: type: boolean kind: type: string + manifest: + properties: + url: + type: string + required: + - url + type: object name: type: string namespace: type: string required: - - chart - enabled - kind - name diff --git a/config/crd/bases/boundless.mirantis.com_blueprints.yaml b/config/crd/bases/boundless.mirantis.com_blueprints.yaml index 1466eef7..3e7c0c1c 100644 --- a/config/crd/bases/boundless.mirantis.com_blueprints.yaml +++ b/config/crd/bases/boundless.mirantis.com_blueprints.yaml @@ -69,12 +69,18 @@ spec: type: boolean kind: type: string + manifest: + properties: + url: + type: string + required: + - url + type: object name: type: string namespace: type: string required: - - chart - enabled - kind - name diff --git a/config/crd/bases/boundless.mirantis.com_manifests.yaml b/config/crd/bases/boundless.mirantis.com_manifests.yaml new file mode 100644 index 00000000..b1372947 --- /dev/null +++ b/config/crd/bases/boundless.mirantis.com_manifests.yaml @@ -0,0 +1,50 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: manifests.boundless.mirantis.com +spec: + group: boundless.mirantis.com + names: + kind: Manifest + listKind: ManifestList + plural: manifests + singular: manifest + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Manifest is the Schema for the manifests 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: ManifestSpec defines the desired state of Manifest + properties: + url: + description: Foo is an example field of Manifest. Edit manifest_types.go + to remove/update + type: string + type: object + status: + description: ManifestStatus defines the observed state of Manifest + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 0c274c7f..625f6af5 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -5,6 +5,7 @@ resources: - bases/boundless.mirantis.com_addons.yaml - bases/boundless.mirantis.com_ingresses.yaml - bases/boundless.mirantis.com_blueprints.yaml +- bases/boundless.mirantis.com_manifests.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -14,6 +15,7 @@ patchesStrategicMerge: #- patches/webhook_in_clusters.yaml #- patches/webhook_in_ingresses.yaml #- patches/webhook_in_blueprints.yaml +#- patches/webhook_in_manifests.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -22,6 +24,7 @@ patchesStrategicMerge: #- patches/cainjection_in_clusters.yaml #- patches/cainjection_in_ingresses.yaml #- patches/cainjection_in_blueprints.yaml +#- patches/cainjection_in_manifests.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_manifests.yaml b/config/crd/patches/cainjection_in_manifests.yaml new file mode 100644 index 00000000..7261cc88 --- /dev/null +++ b/config/crd/patches/cainjection_in_manifests.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: manifests.boundless.mirantis.com diff --git a/config/crd/patches/webhook_in_manifests.yaml b/config/crd/patches/webhook_in_manifests.yaml new file mode 100644 index 00000000..b14ee247 --- /dev/null +++ b/config/crd/patches/webhook_in_manifests.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: manifests.boundless.mirantis.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 5c5f0b84..a031cf79 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,2 +1,8 @@ resources: - manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: ghcr.io/mirantis/boundless-operator + newTag: manifest diff --git a/config/rbac/manifest_editor_role.yaml b/config/rbac/manifest_editor_role.yaml new file mode 100644 index 00000000..826cc476 --- /dev/null +++ b/config/rbac/manifest_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit manifests. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: manifest-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: boundless-operator + app.kubernetes.io/part-of: boundless-operator + app.kubernetes.io/managed-by: kustomize + name: manifest-editor-role +rules: +- apiGroups: + - boundless.mirantis.com + resources: + - manifests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - boundless.mirantis.com + resources: + - manifests/status + verbs: + - get diff --git a/config/rbac/manifest_viewer_role.yaml b/config/rbac/manifest_viewer_role.yaml new file mode 100644 index 00000000..2688054a --- /dev/null +++ b/config/rbac/manifest_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view manifests. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: manifest-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: boundless-operator + app.kubernetes.io/part-of: boundless-operator + app.kubernetes.io/managed-by: kustomize + name: manifest-viewer-role +rules: +- apiGroups: + - boundless.mirantis.com + resources: + - manifests + verbs: + - get + - list + - watch +- apiGroups: + - boundless.mirantis.com + resources: + - manifests/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index ad56f397..19d42820 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -83,3 +83,29 @@ rules: - get - patch - update +- apiGroups: + - boundless.mirantis.com + resources: + - manifests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - boundless.mirantis.com + resources: + - manifests/finalizers + verbs: + - update +- apiGroups: + - boundless.mirantis.com + resources: + - manifests/status + verbs: + - get + - patch + - update diff --git a/config/samples/boundless_v1alpha1_manifest.yaml b/config/samples/boundless_v1alpha1_manifest.yaml new file mode 100644 index 00000000..38cade5a --- /dev/null +++ b/config/samples/boundless_v1alpha1_manifest.yaml @@ -0,0 +1,12 @@ +apiVersion: boundless.mirantis.com/v1alpha1 +kind: Manifest +metadata: + labels: + app.kubernetes.io/name: manifest + app.kubernetes.io/instance: manifest-sample + app.kubernetes.io/part-of: boundless-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: boundless-operator + name: manifest-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index c27c175a..6677d995 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -4,4 +4,5 @@ resources: - boundless_v1alpha1_cluster.yaml - boundless_v1alpha1_ingress.yaml - boundless_v1alpha1_blueprint.yaml +- boundless_v1alpha1_manifest.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/controllers/addon_controller.go b/controllers/addon_controller.go index a7779771..caf489a8 100644 --- a/controllers/addon_controller.go +++ b/controllers/addon_controller.go @@ -2,6 +2,7 @@ package controllers import ( "context" + "fmt" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -12,6 +13,12 @@ import ( boundlessv1alpha1 "github.com/mirantis/boundless-operator/api/v1alpha1" "github.com/mirantis/boundless-operator/pkg/helm" + "github.com/mirantis/boundless-operator/pkg/manifest" +) + +const ( + kindManifest = "manifest" + kindChart = "chart" ) // AddonReconciler reconciles a Addon object @@ -53,53 +60,69 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, err } - chart := helm.Chart{ - Name: instance.Spec.Chart.Name, - Repo: instance.Spec.Chart.Repo, - Version: instance.Spec.Chart.Version, - Set: instance.Spec.Chart.Set, - Values: instance.Spec.Chart.Values, - } + switch instance.Spec.Kind { + case kindChart: + chart := helm.Chart{ + Name: instance.Spec.Chart.Name, + Repo: instance.Spec.Chart.Repo, + Version: instance.Spec.Chart.Version, + Set: instance.Spec.Chart.Set, + Values: instance.Spec.Chart.Values, + } + + logger.Info("Reconciler instance details", "Name", instance.Spec.Chart.Name) - hc := helm.NewHelmChartController(r.Client, logger) + hc := helm.NewHelmChartController(r.Client, logger) - addonFinalizerName := "boundless.mirantis.com/finalizer" + addonFinalizerName := "boundless.mirantis.com/finalizer" - if instance.ObjectMeta.DeletionTimestamp.IsZero() { - // The object is not being deleted, so if it does not have our finalizer, - // then lets add the finalizer and update the object. This is equivalent - // registering our finalizer. - if !controllerutil.ContainsFinalizer(instance, addonFinalizerName) { - controllerutil.AddFinalizer(instance, addonFinalizerName) - if err := r.Update(ctx, instance); err != nil { - return ctrl.Result{}, err + if instance.ObjectMeta.DeletionTimestamp.IsZero() { + // The object is not being deleted, so if it does not have our finalizer, + // then lets add the finalizer and update the object. This is equivalent + // registering our finalizer. + if !controllerutil.ContainsFinalizer(instance, addonFinalizerName) { + controllerutil.AddFinalizer(instance, addonFinalizerName) + if err := r.Update(ctx, instance); err != nil { + return ctrl.Result{}, err + } } - } - } else { - // The object is being deleted - if controllerutil.ContainsFinalizer(instance, addonFinalizerName) { - // our finalizer is present, so lets delete the helm chart - if err := hc.DeleteHelmChart(chart, instance.Spec.Namespace); err != nil { - // if fail to delete the helm chart here, return with error - // so that it can be retried - return ctrl.Result{}, err + } else { + // The object is being deleted + if controllerutil.ContainsFinalizer(instance, addonFinalizerName) { + // our finalizer is present, so lets delete the helm chart + if err := hc.DeleteHelmChart(chart, instance.Spec.Namespace); err != nil { + // if fail to delete the helm chart here, return with error + // so that it can be retried + return ctrl.Result{}, err + } + + // remove our finalizer from the list and update it. + controllerutil.RemoveFinalizer(instance, addonFinalizerName) + if err := r.Update(ctx, instance); err != nil { + return ctrl.Result{}, err + } } - // remove our finalizer from the list and update it. - controllerutil.RemoveFinalizer(instance, addonFinalizerName) - if err := r.Update(ctx, instance); err != nil { - return ctrl.Result{}, err - } + // Stop reconciliation as the item is being deleted + return ctrl.Result{}, nil } - // Stop reconciliation as the item is being deleted - return ctrl.Result{}, nil - } + logger.Info("Creating Addon HelmChart resource", "Name", chart.Name, "Version", chart.Version) + if err := hc.CreateHelmChart(chart, instance.Spec.Namespace); err != nil { + logger.Error(err, "failed to install addon", "Name", chart.Name, "Version", chart.Version) + return ctrl.Result{Requeue: true}, err + } + case kindManifest: + mc := manifest.NewManifestController(r.Client, logger) + err = mc.CreateManifest(instance.Spec.Namespace, instance.Spec.Name, instance.Spec.Manifest.URL) + if err != nil { + logger.Error(err, "failed to install addon via manifest", "URL", instance.Spec.Manifest.URL) + return ctrl.Result{Requeue: true}, err + } - logger.Info("Creating Addon HelmChart resource", "Name", chart.Name, "Version", chart.Version) - if err2 := hc.CreateHelmChart(chart, instance.Spec.Namespace); err2 != nil { - logger.Error(err, "failed to install addon", "Name", chart.Name, "Version", chart.Version) - return ctrl.Result{Requeue: true}, err2 + default: + logger.Info("Unknown AddOn kind", "Kind", instance.Spec.Kind) + return ctrl.Result{Requeue: false}, fmt.Errorf("Unknown AddOn Kind: %w", err) } logger.Info("Finished reconcile request on MkeAddon instance", "Name", req.Name) diff --git a/controllers/blueprint_controller.go b/controllers/blueprint_controller.go index fedcb4e7..822ccebf 100644 --- a/controllers/blueprint_controller.go +++ b/controllers/blueprint_controller.go @@ -222,6 +222,7 @@ func ingressResource(spec *boundlessv1alpha1.IngressSpec) *boundlessv1alpha1.Ing } func addonResource(spec *boundlessv1alpha1.AddonSpec) *boundlessv1alpha1.Addon { + return &boundlessv1alpha1.Addon{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, @@ -230,13 +231,17 @@ func addonResource(spec *boundlessv1alpha1.AddonSpec) *boundlessv1alpha1.Addon { Spec: boundlessv1alpha1.AddonSpec{ Name: spec.Name, Namespace: spec.Namespace, - Chart: boundlessv1alpha1.Chart{ + Kind: spec.Kind, + Chart: boundlessv1alpha1.ChartInfo{ Name: spec.Chart.Name, Repo: spec.Chart.Repo, Version: spec.Chart.Version, Set: spec.Chart.Set, Values: spec.Chart.Values, }, + Manifest: boundlessv1alpha1.ManifestInfo{ + URL: spec.Manifest.URL, + }, }, } } diff --git a/controllers/manifest_controller.go b/controllers/manifest_controller.go new file mode 100644 index 00000000..94bcb44f --- /dev/null +++ b/controllers/manifest_controller.go @@ -0,0 +1,47 @@ +package controllers + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + boundlessv1alpha1 "github.com/mirantis/boundless-operator/api/v1alpha1" +) + +// ManifestReconciler reconciles a Manifest object +type ManifestReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=boundless.mirantis.com,resources=manifests,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=boundless.mirantis.com,resources=manifests/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=boundless.mirantis.com,resources=manifests/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 Manifest 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.1/pkg/reconcile +func (r *ManifestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + logger := log.FromContext(ctx) + logger.Info("Reconcile request on Manifest instance") + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ManifestReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&boundlessv1alpha1.Manifest{}). + Complete(r) +} diff --git a/deploy/static/boundless-operator.yaml b/deploy/static/boundless-operator.yaml index d438c5ab..31fa577f 100644 --- a/deploy/static/boundless-operator.yaml +++ b/deploy/static/boundless-operator.yaml @@ -69,12 +69,18 @@ spec: type: boolean kind: type: string + manifest: + properties: + url: + type: string + required: + - url + type: object name: type: string namespace: type: string required: - - chart - enabled - kind - name @@ -153,12 +159,18 @@ spec: type: boolean kind: type: string + manifest: + properties: + url: + type: string + required: + - url + type: object name: type: string namespace: type: string required: - - chart - enabled - kind - name @@ -249,6 +261,51 @@ spec: subresources: status: {} --- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: manifests.boundless.mirantis.com +spec: + group: boundless.mirantis.com + names: + kind: Manifest + listKind: ManifestList + plural: manifests + singular: manifest + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Manifest is the Schema for the manifests 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: ManifestSpec defines the desired state of Manifest + properties: + url: + description: Foo is an example field of Manifest. Edit manifest_types.go to remove/update + type: string + type: object + status: + description: ManifestStatus defines the observed state of Manifest + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- apiVersion: v1 kind: ServiceAccount metadata: @@ -391,6 +448,32 @@ rules: - get - patch - update +- apiGroups: + - boundless.mirantis.com + resources: + - manifests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - boundless.mirantis.com + resources: + - manifests/finalizers + verbs: + - update +- apiGroups: + - boundless.mirantis.com + resources: + - manifests/status + verbs: + - get + - patch + - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole diff --git a/main.go b/main.go index 50c6680e..02c88bc4 100644 --- a/main.go +++ b/main.go @@ -99,6 +99,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Ingress") os.Exit(1) } + if err = (&controllers.ManifestReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Manifest") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go new file mode 100644 index 00000000..a39a2d7b --- /dev/null +++ b/pkg/manifest/manifest.go @@ -0,0 +1,411 @@ +package manifest + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + + boundlessv1alpha1 "github.com/mirantis/boundless-operator/api/v1alpha1" + adm_v1 "k8s.io/api/admissionregistration/v1" + apps_v1 "k8s.io/api/apps/v1" + core_v1 "k8s.io/api/core/v1" + policy_v1 "k8s.io/api/policy/v1" + rbac_v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" +) + +type ManifestController struct { + client client.Client + logger logr.Logger +} + +func NewManifestController(client client.Client, logger logr.Logger) *ManifestController { + return &ManifestController{ + client: client, + logger: logger, + } +} + +func (mc *ManifestController) CreateManifest(namespace, name, url string) error { + + var client http.Client + + // Run http get request to fetch the contents of the manifest file + resp, err := client.Get(url) + if err != nil { + mc.logger.Error(err, "failed to run Unable to read response") + return err + } + + defer resp.Body.Close() + + var bodyBytes []byte + if resp.StatusCode == http.StatusOK { + bodyBytes, err = io.ReadAll(resp.Body) + if err != nil { + mc.logger.Error(err, "failed to read http response body") + return err + } + + } else { + mc.logger.Error(err, "failure in http get request", "ResponseCode", resp.StatusCode) + return err + } + + return mc.createOrUpdateManifest(namespace, name, url, bodyBytes) + +} + +func (mc *ManifestController) createOrUpdateManifest(namespace, name, url string, bodyBytes []byte) error { + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + existing, err := mc.getExistingManifest(ctx, namespace, name) + if err != nil { + return err + } + + if existing != nil { + // ToDo : add code for update + mc.logger.Info("manifest crd already exists", "Manifest", existing) + return nil + + } else { + mc.logger.Info("manifest crd does not exist, creating", "ManifestName", name, "Namespace", namespace) + + // Deserialize the manifest contents and fetch all the objects + _, err := mc.CreateManifestCRD(namespace, name, url, bodyBytes) + + if err != nil { + mc.logger.Error(err, "failed to create manifest CRD") + return err + } + } + + return nil +} + +func (mc *ManifestController) getExistingManifest(ctx context.Context, namespace, name string) (*boundlessv1alpha1.Manifest, error) { + key := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + existing := &boundlessv1alpha1.Manifest{} + err := mc.client.Get(ctx, key, existing) + if err != nil { + if strings.Contains(err.Error(), "not found") { + mc.logger.Info("manifest does not exist", "Namespace", namespace, "ManifestName", name) + return nil, nil + } else { + return nil, fmt.Errorf("failed to get existing manifest: %w", err) + } + } + return existing, nil +} + +func (mc *ManifestController) CreateManifestCRD(namespace, name, url string, data []byte) ([]*runtime.Object, error) { + apiextensionsv1.AddToScheme(scheme.Scheme) + apiextensionsv1beta1.AddToScheme(scheme.Scheme) + adm_v1.AddToScheme(scheme.Scheme) + apps_v1.AddToScheme(scheme.Scheme) + core_v1.AddToScheme(scheme.Scheme) + policy_v1.AddToScheme(scheme.Scheme) + rbac_v1.AddToScheme(scheme.Scheme) + + decoder := scheme.Codecs.UniversalDeserializer() + + for _, obj := range strings.Split(string(data), "---") { + if obj != "" { + runtimeObject, groupVersionKind, err := decoder.Decode([]byte(obj), nil, nil) + if err != nil { + mc.logger.Info("Failed to decode yaml:", "Error", err) + return nil, err + } + + mc.logger.Info("Decode details", "runtimeObject", runtimeObject, "groupVersionKind", groupVersionKind) + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + mc.logger.Info("The object recvd is:", "Kind", groupVersionKind.Kind) + + switch groupVersionKind.Kind { + case "Namespace": + // @TODO: create the namespace if it doesn't exist + namespaceObj := convertToNamespaceObject(runtimeObject) + err := mc.client.Create(ctx, namespaceObj) + if err != nil { + if strings.Contains(err.Error(), "already exists") { + mc.logger.Info("Namespace already exists:", "Namespace", namespaceObj.Name) + return nil, nil + } + return nil, err + } + mc.logger.Info("Namespace created successfully:", "Namespace", namespaceObj.Name) + + case "Service": + // @TODO: create the service if it doesn't exist + serviceObj := convertToServiceObject(runtimeObject) + if serviceObj.Namespace == "" { + serviceObj.Namespace = "default" + } + err = mc.client.Create(ctx, serviceObj) + if err != nil { + mc.logger.Info("Failed to create service:", "Error", err) + return nil, err + } + mc.logger.Info("Service created successfully:", "Service", serviceObj.Name) + + case "Deployment": + // @TODO: create the deployment if it doesn't exist + deploymentObj := convertToDeploymentObject(runtimeObject) + if deploymentObj.Namespace == "" { + deploymentObj.Namespace = "default" + } + err = mc.client.Create(ctx, deploymentObj) + if err != nil { + mc.logger.Info("Failed to create deployment:", "Error", err) + return nil, err + } + mc.logger.Info("Deployment created successfully:", "Deployment", deploymentObj.Name) + + case "DaemonSet": + // @TODO: create the daemonSet if it doesn't exist + daemonsetObj := convertToDaemonsetObject(runtimeObject) + if daemonsetObj.Namespace == "" { + daemonsetObj.Namespace = "default" + } + err = mc.client.Create(ctx, daemonsetObj) + if err != nil { + mc.logger.Info("Failed to create daemonset:", "Error", err) + return nil, err + } + mc.logger.Info("daemonset created successfully:", "Daemonset", daemonsetObj.Name) + + case "PodDisruptionBudget": + pdbObj := convertToPodDisruptionBudget(runtimeObject) + if pdbObj.Namespace == "" { + pdbObj.Namespace = "default" + } + err = mc.client.Create(ctx, pdbObj) + if err != nil { + mc.logger.Info("Failed to create pod disruption budget:", "Error", err) + return nil, err + } + mc.logger.Info("Pod disruption budget created successfully:", "PodDisruptionBudget", pdbObj.Name) + + case "ServiceAccount": + serviceAccObj := convertToServiceAccount(runtimeObject) + if serviceAccObj.Namespace == "" { + serviceAccObj.Namespace = "default" + } + err = mc.client.Create(ctx, serviceAccObj) + if err != nil { + mc.logger.Info("Failed to create service account:", "Error", err) + return nil, err + } + mc.logger.Info("service account created successfully:", "ServiceAccount", serviceAccObj.Name) + + case "Role": + roleObj := convertToRoleObject(runtimeObject) + if roleObj.Namespace == "" { + roleObj.Namespace = "default" + } + err = mc.client.Create(ctx, roleObj) + if err != nil { + mc.logger.Info("Failed to create role:", "Error", err) + return nil, err + } + mc.logger.Info("Role created successfully:", "Role", roleObj.Name) + + case "ClusterRole": + clusterRoleObj := convertToClusterRoleObject(runtimeObject) + if clusterRoleObj.Namespace == "" { + clusterRoleObj.Namespace = "default" + } + err = mc.client.Create(ctx, clusterRoleObj) + if err != nil { + mc.logger.Info("Failed to create clusterrole:", "Error", err) + return nil, err + } + mc.logger.Info("ClusterRole created successfully:", "Role", clusterRoleObj.Name) + + case "Secret": + secretObj := convertToSecretObject(runtimeObject) + if secretObj.Namespace == "" { + secretObj.Namespace = "default" + } + err = mc.client.Create(ctx, secretObj) + if err != nil { + mc.logger.Info("Failed to create secret:", "Error", err) + return nil, err + } + mc.logger.Info("secret created successfully:", "Secret", secretObj.Name) + + case "RoleBinding": + roleBindingObj := convertToRoleBindingObject(runtimeObject) + if roleBindingObj.Namespace == "" { + roleBindingObj.Namespace = "default" + } + err = mc.client.Create(ctx, roleBindingObj) + if err != nil { + mc.logger.Info("Failed to create role binding:", "Error", err) + return nil, err + } + mc.logger.Info("role binding created successfully:", "RoleBinding", roleBindingObj.Name) + + case "ClusterRoleBinding": + clusterRoleBindingObj := convertToClusterRoleBindingObject(runtimeObject) + mc.logger.Info("Creating cluster role binding", "Name", clusterRoleBindingObj.Name) + + err = mc.client.Create(ctx, clusterRoleBindingObj) + if err != nil { + mc.logger.Info("Failed to create cluster role binding:", "Error", err) + return nil, err + } + mc.logger.Info("cluster role binding created successfully:", "ClusterRoleBinding", clusterRoleBindingObj.Name) + + case "ConfigMap": + configMapObj := convertToConfigMapObject(runtimeObject) + if configMapObj.Namespace == "" { + configMapObj.Namespace = "default" + } + err = mc.client.Create(ctx, configMapObj) + if err != nil { + mc.logger.Info("Failed to create configmap:", "Error", err) + return nil, err + } + mc.logger.Info("configmap created successfully:", "ConfigMap", configMapObj.Name) + + case "CustomResourceDefinition": + crdObj := convertToCRDObject(runtimeObject) + + err = mc.client.Create(ctx, crdObj) + if err != nil { + mc.logger.Info("Failed to create crd:", "Error", err) + return nil, err + } + mc.logger.Info("crd created successfully:", "CRD", crdObj.Name) + + case "ValidatingWebhookConfiguration": + webhookObj := convertToValidatingWebhookObject(runtimeObject) + + err = mc.client.Create(ctx, webhookObj) + if err != nil { + mc.logger.Info("Failed to create validating webhook:", "Error", err) + return nil, err + } + mc.logger.Info("validating webhook created successfully:", "ValidatingWebhook", webhookObj.Name) + + default: + mc.logger.Info("Object kind not supported", "Kind", groupVersionKind.Kind) + } + } + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + manifest := &boundlessv1alpha1.Manifest{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: boundlessv1alpha1.ManifestSpec{ + Url: url, + }, + } + + err := mc.client.Create(ctx, manifest) + if err != nil { + mc.logger.Info("failed to create manifest crd", "Error", err) + return nil, err + } + mc.logger.Info("manifest created successfully", "ManifestName", "name") + + return nil, nil +} + +func convertToNamespaceObject(obj runtime.Object) *core_v1.Namespace { + myobj := obj.(*core_v1.Namespace) + return myobj +} + +func convertToServiceObject(obj runtime.Object) *core_v1.Service { + myobj := obj.(*core_v1.Service) + return myobj +} + +func convertToDeploymentObject(obj runtime.Object) *apps_v1.Deployment { + myobj := obj.(*apps_v1.Deployment) + return myobj +} + +func convertToPodDisruptionBudget(obj runtime.Object) *policy_v1.PodDisruptionBudget { + myobj := obj.(*policy_v1.PodDisruptionBudget) + return myobj +} + +func convertToServiceAccount(obj runtime.Object) *core_v1.ServiceAccount { + myobj := obj.(*core_v1.ServiceAccount) + return myobj +} + +func convertToCRDObject(obj runtime.Object) *apiextensionsv1.CustomResourceDefinition { + myobj := obj.(*apiextensionsv1.CustomResourceDefinition) + return myobj +} + +func convertToDaemonsetObject(obj runtime.Object) *apps_v1.DaemonSet { + myobj := obj.(*apps_v1.DaemonSet) + return myobj +} + +func convertToRoleObject(obj runtime.Object) *rbac_v1.Role { + myobj := obj.(*rbac_v1.Role) + return myobj +} + +func convertToClusterRoleObject(obj runtime.Object) *rbac_v1.ClusterRole { + myobj := obj.(*rbac_v1.ClusterRole) + return myobj +} + +func convertToRoleBindingObject(obj runtime.Object) *rbac_v1.RoleBinding { + myobj := obj.(*rbac_v1.RoleBinding) + return myobj +} + +func convertToClusterRoleBindingObject(obj runtime.Object) *rbac_v1.ClusterRoleBinding { + myobj := obj.(*rbac_v1.ClusterRoleBinding) + return myobj +} + +func convertToSecretObject(obj runtime.Object) *core_v1.Secret { + myobj := obj.(*core_v1.Secret) + return myobj +} + +func convertToConfigMapObject(obj runtime.Object) *core_v1.ConfigMap { + myobj := obj.(*core_v1.ConfigMap) + return myobj +} + +func convertToValidatingWebhookObject(obj runtime.Object) *adm_v1.ValidatingWebhookConfiguration { + myobj := obj.(*adm_v1.ValidatingWebhookConfiguration) + return myobj +}