diff --git a/.ci/scripts/local.sh b/.ci/scripts/local.sh index 397ea2d4a..0239654a1 100755 --- a/.ci/scripts/local.sh +++ b/.ci/scripts/local.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -kustomize build config/local | kubectl apply -f - +kustomize build config/local | kubectl apply --server-side=true -f - make manifests generate fmt vet CR_KIND=$1 CR_DOMAIN=$2 CR_PLURAL=$3 APP_IMAGE=$4 WEB_IMAGE=$5 if [[ "$CI_TEST" == "true" ]] ; then make build diff --git a/CHANGES/821.feature b/CHANGES/821.feature new file mode 100644 index 000000000..724e80221 --- /dev/null +++ b/CHANGES/821.feature @@ -0,0 +1 @@ +Added a feature to deploy and sync Galaxy execution environments. diff --git a/Makefile b/Makefile index 48cae9e11..654a8f948 100644 --- a/Makefile +++ b/Makefile @@ -210,7 +210,7 @@ endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | kubectl apply -f - + $(KUSTOMIZE) build config/crd | kubectl apply --server-side=true -f - .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. @@ -224,7 +224,7 @@ local: kustomize ## Run controller in the K8s cluster specified in ~/.kube/confi deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} cd config/default && $(KUSTOMIZE) edit set namespace ${NAMESPACE} - $(KUSTOMIZE) build config/default | kubectl apply -f - + $(KUSTOMIZE) build config/default | kubectl apply --server-side=true -f - .PHONY: undeploy undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. diff --git a/api/v1alpha1/repo_manager_types.go b/api/v1alpha1/repo_manager_types.go index f00c93689..c2e64263d 100644 --- a/api/v1alpha1/repo_manager_types.go +++ b/api/v1alpha1/repo_manager_types.go @@ -305,6 +305,18 @@ type PulpSpec struct { // +kubebuilder:default:=false // +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:hidden"} TrustedCa bool `json:"mount_trusted_ca,omitempty"` + + // Define if the operator should or should not deploy the default Galaxy Execution Environments. + // Default: false + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:hidden"} + DeployGalaxyEEDefaults bool `json:"deploy_galaxy_ee_defaults,omitempty"` + + // Name of the ConfigMap with the list of Galaxy Execution Environments that should be synchronized. + // Default: ee-default-images + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:hidden"} + GalaxyEEDefaults string `json:"galaxy_ee_defaults,omitempty"` } // Api defines desired state of pulpcore-api resources diff --git a/config/crd/bases/repo-manager.pulpproject.org_pulps.yaml b/config/crd/bases/repo-manager.pulpproject.org_pulps.yaml index 5d997c13b..d98a83fba 100644 --- a/config/crd/bases/repo-manager.pulpproject.org_pulps.yaml +++ b/config/crd/bases/repo-manager.pulpproject.org_pulps.yaml @@ -5779,6 +5779,10 @@ spec: description: 'Secret where the Fernet symmetric encryption key is stored. Default: -"-db-fields-encryption"' type: string + deploy_galaxy_ee_defaults: + description: 'Define if the operator should or should not deploy the + default Galaxy Execution Environments. Default: false' + type: boolean deployment_type: default: pulp description: 'Name of the deployment type. Default: "pulp"' @@ -5800,6 +5804,10 @@ spec: file_storage_storage_class: description: Storage class to use for the file persistentVolumeClaim type: string + galaxy_ee_defaults: + description: 'Name of the ConfigMap with the list of Galaxy Execution + Environments that should be synchronized. Default: ee-default-images' + type: string haproxy_timeout: description: 'The timeout for HAProxy. Default: "180s"' type: string diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 8dcb6b9d4..916992e09 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -51,6 +51,18 @@ rules: - patch - update - watch +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: diff --git a/controllers/repo_manager/README.md b/controllers/repo_manager/README.md index cb12ba999..f51b477ad 100644 --- a/controllers/repo_manager/README.md +++ b/controllers/repo_manager/README.md @@ -182,6 +182,8 @@ PulpSpec defines the desired state of Pulp | image_pull_secrets | Image pull secrets for container images. Default: [] | []string | false | | sso_secret | Secret where Single Sign-on configuration can be found | string | false | | mount_trusted_ca | Define if the operator should or should not mount the custom CA certificates added to the cluster via cluster-wide proxy config. Default: false | bool | false | +| deploy_galaxy_ee_defaults | Define if the operator should or should not deploy the default Galaxy Execution Environments. Default: false | bool | false | +| galaxy_ee_defaults | Name of the ConfigMap with the list of Galaxy Execution Environments that should be synchronized. Default: ee-default-images | string | false | [Back to Custom Resources](#custom-resources) diff --git a/controllers/repo_manager/api.go b/controllers/repo_manager/api.go index cdb17f197..746569ff7 100644 --- a/controllers/repo_manager/api.go +++ b/controllers/repo_manager/api.go @@ -21,7 +21,6 @@ import ( "fmt" "os" "strconv" - "strings" "time" "golang.org/x/text/cases" @@ -35,7 +34,6 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "github.com/go-logr/logr" - configv1 "github.com/openshift/api/config/v1" repomanagerv1alpha1 "github.com/pulp/pulp-operator/api/v1alpha1" "github.com/pulp/pulp-operator/controllers" ctrl "sigs.k8s.io/controller-runtime" @@ -789,19 +787,7 @@ func pulpServerSecret(resources FunctionResources) client.Object { } // Handling user facing URLs - rootUrl := "http://" + resources.Pulp.Name + "-web-svc." + resources.Pulp.Namespace + ".svc.cluster.local:24880" - if strings.ToLower(resources.Pulp.Spec.IngressType) == "ingress" { - rootUrl = "https://" + resources.Pulp.Spec.IngressHost - } - if strings.ToLower(resources.Pulp.Spec.IngressType) == "route" { - if len(resources.Pulp.Spec.RouteHost) == 0 { - ingress := &configv1.Ingress{} - resources.RepoManagerReconciler.Get(resources.Context, types.NamespacedName{Name: "cluster"}, ingress) - rootUrl = "https://" + resources.Pulp.Name + "." + ingress.Spec.Domain - } else { - rootUrl = "https://" + resources.Pulp.Spec.RouteHost - } - } + rootUrl := getRootURL(resources) // default settings.py configuration var pulp_settings = `DB_ENCRYPTION_KEY = "/etc/pulp/keys/database_fields.symmetric.key" diff --git a/controllers/repo_manager/controller.go b/controllers/repo_manager/controller.go index c938c3932..e00798700 100644 --- a/controllers/repo_manager/controller.go +++ b/controllers/repo_manager/controller.go @@ -24,6 +24,7 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" policy "k8s.io/api/policy/v1" @@ -70,6 +71,7 @@ type RepoManagerReconciler struct { //+kubebuilder:rbac:groups=core,namespace=pulp-operator-system,resources=configmaps;secrets;services;persistentvolumeclaims,verbs=create;update;patch;delete;watch;get;list; //+kubebuilder:rbac:groups="",namespace=pulp-operator-system,resources=events,verbs=create;patch //+kubebuilder:rbac:groups=policy,namespace=pulp-operator-system,resources=poddisruptionbudgets,verbs=get;list;create;delete;patch;update;watch +//+kubebuilder:rbac:groups=batch,namespace=pulp-operator-system,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -305,6 +307,11 @@ func (r *RepoManagerReconciler) Reconcile(ctx context.Context, req ctrl.Request) return pulpController, err } + log.V(1).Info("Running Galaxy EE tasks") + if pulpController, err := galaxyEECronjob(FunctionResources{ctx, pulp, log, r}); needsRequeue(err, pulpController) { + return pulpController, err + } + log.V(1).Info("Running status tasks") pulpController, err = r.pulpStatus(ctx, pulp, log) if needsRequeue(err, pulpController) { @@ -332,6 +339,7 @@ func (r *RepoManagerReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&corev1.ConfigMap{}). Owns(&policy.PodDisruptionBudget{}). Owns(&corev1.ServiceAccount{}). + Owns(&batchv1.CronJob{}, builder.WithPredicates(ignoreCronjobStatus())). Owns(&netv1.Ingress{}) if IsOpenShift, _ := controllers.IsOpenShift(); IsOpenShift { diff --git a/controllers/repo_manager/galaxy.go b/controllers/repo_manager/galaxy.go new file mode 100644 index 000000000..79a2499b2 --- /dev/null +++ b/controllers/repo_manager/galaxy.go @@ -0,0 +1,170 @@ +package repo_manager + +import ( + "net/url" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const defaultConfigMapName = "ee-default-images" + +// GalaxyResource has the definition and function to provision galaxy objects +type GalaxyResource struct { + Definition ResourceDefinition + Function func(FunctionResources) client.Object +} + +// galaxyEECronjob creates the cronjob used to provide default execution environments +func galaxyEECronjob(resources FunctionResources) (ctrl.Result, error) { + + // ignore this func if deployment type is pulp + if resources.Pulp.Spec.DeploymentType != "galaxy" { + return ctrl.Result{}, nil + } + + // ignore this method if defined to not deploy default images + if !resources.Pulp.Spec.DeployGalaxyEEDefaults { + return ctrl.Result{}, nil + } + + // list of galaxy resources that should be provisioned + newResources := []GalaxyResource{ + // galaxy configmap + {Definition: ResourceDefinition{Context: resources.Context, Type: &corev1.ConfigMap{}, Name: getConfigMapName(resources), Alias: "", ConditionType: "", Pulp: resources.Pulp}, Function: galaxyEEConfigMap}, + // galaxy cronjob + {ResourceDefinition{resources.Context, &batchv1.CronJob{}, resources.Pulp.Name + "-ee-defaults", "", "", resources.Pulp}, galaxyEECronJob}, + } + + // create resources + for _, resource := range newResources { + requeue, err := resources.RepoManagerReconciler.createPulpResource(resource.Definition, resource.Function) + if err != nil { + return ctrl.Result{}, err + } else if requeue { + return ctrl.Result{Requeue: true}, nil + } + } + + return ctrl.Result{}, nil +} + +// getConfigMapName returns the name of ConfigMap with the list of EE that should be synchronized +func getConfigMapName(resources FunctionResources) string { + galaxyEEConfigmapName := defaultConfigMapName + if len(resources.Pulp.Spec.GalaxyEEDefaults) > 0 { + galaxyEEConfigmapName = resources.Pulp.Spec.GalaxyEEDefaults + } + + return galaxyEEConfigmapName +} + +// galaxyEEConfigMap returns a default ConfigMap with the list of default images +// that should be synced +func galaxyEEConfigMap(resources FunctionResources) client.Object { + images := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{ + Name: defaultConfigMapName, + Namespace: resources.Namespace, + }, + Data: map[string]string{ + "images.yaml": `quay.io: + images-by-tag-regex: + fedora/fedora-minimal: ^latest$ + fedora/fedora: ^latest$`, + }, + } + ctrl.SetControllerReference(resources.Pulp, images, resources.RepoManagerReconciler.Scheme) + return images +} + +// galaxyEECronJob returns a CronJob that will be used to trigger a sync of +// EE images from time to time +func galaxyEECronJob(resources FunctionResources) client.Object { + + // image used to run the sync + skopeoImage := "quay.io/skopeo/stable" + + // galaxy image registry host + rootURL, _ := url.Parse(getRootURL(resources)) + + successfulHistory := int32(1) + failedHistory := int32(2) + + cronJob := &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: resources.Pulp.Name + "-ee-defaults", + Namespace: resources.Pulp.Namespace, + }, + Spec: batchv1.CronJobSpec{ + Schedule: "*/2 * * * *", + SuccessfulJobsHistoryLimit: &successfulHistory, + FailedJobsHistoryLimit: &failedHistory, + JobTemplate: batchv1.JobTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: resources.Pulp.Name + "-ee-defaults", + Namespace: resources.Pulp.Namespace, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: resources.Pulp.Name + "-ee-defaults", + Namespace: resources.Pulp.Namespace, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{{ + Name: "skopeo", + Image: skopeoImage, + ImagePullPolicy: corev1.PullAlways, + Env: []corev1.EnvVar{ + {Name: "USERNAME", Value: "admin"}, + {Name: "PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.Pulp.Spec.AdminPasswordSecret, + }, + Key: "password", + }, + }, + }, + }, + Args: []string{ + "--debug", "sync", "--dest", "docker", "--src", "yaml", "--retry-times", "3", "--dest-creds", "$(USERNAME):$(PASSWORD)", "--dest-tls-verify=false", "--keep-going", "/images.yaml", rootURL.Host + "/", + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "ee-default-images", + MountPath: "/images.yaml", + SubPath: "images.yaml", + ReadOnly: true, + }, + }, + }}, + Volumes: []corev1.Volume{ + { + Name: "ee-default-images", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: getConfigMapName(resources), + }, + Items: []corev1.KeyToPath{ + {Key: "images.yaml", Path: "images.yaml"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + ctrl.SetControllerReference(resources.Pulp, cronJob, resources.RepoManagerReconciler.Scheme) + return cronJob +} diff --git a/controllers/repo_manager/utils.go b/controllers/repo_manager/utils.go index bcac1a05e..32a838285 100644 --- a/controllers/repo_manager/utils.go +++ b/controllers/repo_manager/utils.go @@ -18,6 +18,7 @@ import ( "strings" "github.com/go-logr/logr" + configv1 "github.com/openshift/api/config/v1" routev1 "github.com/openshift/api/route/v1" repomanagerv1alpha1 "github.com/pulp/pulp-operator/api/v1alpha1" "github.com/pulp/pulp-operator/controllers" @@ -25,6 +26,7 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -34,6 +36,8 @@ import ( "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" ) const ( @@ -650,6 +654,12 @@ func (r *RepoManagerReconciler) createPulpResource(resource ResourceDefinition, case *corev1.PersistentVolumeClaim: object = resourceType objKind = "PVC" + case *corev1.ConfigMap: + object = resourceType + objKind = "ConfigMap" + case *batchv1.CronJob: + object = resourceType + objKind = "CronJob" } // define the list of parameters to pass to "provisioner" function @@ -712,3 +722,35 @@ func isIngress(pulp *repomanagerv1alpha1.Pulp) bool { func (r *RepoManagerReconciler) isNginxIngress(pulp *repomanagerv1alpha1.Pulp) bool { return isIngress(pulp) && controllers.IsNginxIngressSupported(r, pulp.Spec.IngressClassName) } + +// getRootURL handles user facing URLs +func getRootURL(resource FunctionResources) string { + if strings.ToLower(resource.Pulp.Spec.IngressType) == "ingress" { + return "https://" + resource.Pulp.Spec.IngressHost + } + if strings.ToLower(resource.Pulp.Spec.IngressType) == "route" { + if len(resource.Pulp.Spec.RouteHost) == 0 { + ingress := &configv1.Ingress{} + resource.RepoManagerReconciler.Get(resource.Context, types.NamespacedName{Name: "cluster"}, ingress) + return "https://" + resource.Pulp.Name + "." + ingress.Spec.Domain + } else { + return "https://" + resource.Pulp.Spec.RouteHost + } + } + + return "http://" + resource.Pulp.Name + "-web-svc." + resource.Pulp.Namespace + ".svc.cluster.local:24880" +} + +// ignoreUpdateCRStatusPredicate filters update events on pulpbackup CR status +func ignoreCronjobStatus() predicate.Predicate { + return predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + oldObject := e.ObjectOld.(*batchv1.CronJob) + newObject := e.ObjectNew.(*batchv1.CronJob) + + // if old cronjob.status != new cronjob.status return false, which will instruct + // the controller to do nothing on this update event + return reflect.DeepEqual(oldObject.Status, newObject.Status) + }, + } +} diff --git a/docs/configuring/galaxy.md b/docs/configuring/galaxy.md new file mode 100644 index 000000000..a3b583a8e --- /dev/null +++ b/docs/configuring/galaxy.md @@ -0,0 +1,83 @@ +# Galaxy + +Pulp Operator can also be used to deploy [Galaxy](https://galaxyng.netlify.app/), a Pulp plugin to support hosting your very own Ansible Galaxy server. + +## Deploy and Sync default EE images + +It is possible to configure the operator to automatically deploy a set of *Execution Environments*. +It does so by periodic running a [*skopeo sync*](https://github.com/containers/skopeo/blob/main/docs/skopeo-sync.1.md#name) command through a k8s [`CronJob`](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/). + +To enable this feature, update Pulp CR field `deploy_galaxy_ee_defaults`: +``` +... +spec: + deploy_galaxy_ee_defaults: true +... +``` + +By default, if it is not defined, it will be considered *false*. + + +!!! Info + List of default images synchronized: + + * quay.io/fedora/fedora + * quay.io/fedora/fedora-minimal + +### Configuring the list of images to be synced + +If not provided, the operator will create a `ConfigMap` called `ee-default-images` with a custom list of *Execution Environments* to be synchronized. + +During the installation, it is also possible to define the name of a custom `ConfigMap` using the `galaxy_ee_defaults` field: +``` +... +spec: + deploy_galaxy_ee_defaults: true + galaxy_ee_defaults: +... +``` + +Here is an example of how to create the `ConfigMap`: +``` +$ kubectl apply -f-<- +EOF +``` + +The `ConfigMap` must have the following structure: + +* a key named `images.yaml` +* a [yaml content](https://github.com/containers/skopeo/blob/main/docs/skopeo-sync.1.md#yaml-file-content-used-source-for---src-yaml) with the list of images to be copied + +Check [skopeo repo doc](https://github.com/containers/skopeo/blob/main/docs/skopeo-sync.1.md#yaml-file-content-used-source-for---src-yaml) for more information on the YAML file content format. + + +### Configuring the CronJob resource + +When `deploy_galaxy_ee_defaults` is set true, a `CronJob` resource will be created to schedule `Jobs` that will provision `Pods` to run the *`skopeo sync`* command. +By default, the sync is scheduled to run every two minutes, but it can be changed through the `.spec.schedule` field from `CronJob`: +``` +apiVersion: batch/v1 +kind: CronJob +metadata: + name: my-test-cronjob +spec: + schedule: "*/2 * * * *" +``` + +See Kubernetes `CronJob` documentation for more information of the fields available: [https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/). + +!!! Note + The current version of Pulp Operator is **not** reconciling the `ConfigMap` and `CronJob` + used to sync the images. So, after deploying these resource you can modify it directly and + the operator will not try to rollback the changes. If you wish to discard any change and use + the default values, just delete the `CronJob` and the operator will re-provision a new one. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 71dbfe680..f7eb677dc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,6 +46,7 @@ nav: - Pod Disruption Budget: configuring/pdb.md - Secrets: configuring/secrets.md - Disabling Reconciliation: configuring/unmanaged.md + - Galaxy: configuring/galaxy.md - Backup and Restore: - Overview: backup_and_restore/overview.md - Configuring and Running: backup_and_restore/config_running.md