Commit 55046409 authored by Konstantinos Samaras-Tsakiris's avatar Konstantinos Samaras-Tsakiris
Browse files

Merge branch '36-router-shard' into 'master'

Add router shard annotations to routes

Closes #36

See merge request !23
parents ba006578 2ffea18a
Pipeline #2309716 failed with stages
in 3 minutes and 4 seconds
......@@ -36,6 +36,13 @@ type DrupalSiteSpec struct {
// Environment defines the drupal site environments
// +kubebuilder:validation:Required
Environment `json:"environment"`
// We must set MaxLength to 63 characters as this value is stored in Kubernetes labels and there is limited
// length as mentioned in
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
// +kubebuilder:validation:MaxLength=63
// +optional
AssignedRouterShard string `json:"assignedRouterShard,omitempty"`
}
// Environment defines the environment field in DrupalSite
......@@ -47,8 +54,10 @@ type Environment struct {
// ExtraConfigRepo passes on the git url with advanced configuration to the DrupalSite S2I functionality
// TODO: support branches https://gitlab.cern.ch/drupal/paas/drupalsite-operator/-/issues/28
// +kubebuilder:validation:Pattern=`[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)`
// +optional
ExtraConfigRepo string `json:"extraConfigRepo,omitempty"`
// ImageOverride overrides the image urls in the DrupalSite deployment for the fields that are set
// +optional
ImageOverride `json:"imageOverride,omitempty"`
}
......@@ -58,19 +67,16 @@ type Environment struct {
type ImageOverride struct {
// Sitebuilder overrides the Sitebuilder image url in the DrupalSite deployment
// +kubebuilder:validation:Pattern=`[a-z0-9]+(?:[\/._-][a-z0-9]+)*.`
// +optional
Sitebuilder string `json:"siteBuilder,omitempty"`
// Note: Overrides for the nginx and php images might be added if needed
}
// DrupalSiteStatus defines the observed state of DrupalSite
type DrupalSiteStatus struct {
// Phase aggregates the information from all the conditions and reports on the lifecycle phase of the resource
// Enum: {Creating,Created,Deleted}
Phase string `json:"phase,omitempty"`
// Conditions specifies different conditions based on the DrupalSite status
// +kubebuilder:validation:type=array
// +optional
Conditions status.Conditions `json:"conditions,omitempty"`
}
......
......@@ -23,6 +23,9 @@ spec:
containers:
- args:
- --leader-elect
{{- range .Values.assignableRouterShards }}
- --assignable-router-shard={{ . }}
{{- end }}
command:
- /manager
image: {{ .Values.image | quote }}
......
......@@ -18,3 +18,5 @@ resources:
# Operator-specific configuration
drupalsiteOperator:
drupalRuntimeRepo: "https://gitlab.cern.ch/drupal/paas/drupal-runtime.git"
assignableRouterShards:
- apps-shard-1
......@@ -36,6 +36,12 @@ spec:
spec:
description: DrupalSiteSpec defines the desired state of DrupalSite
properties:
assignedRouterShard:
description: We must set MaxLength to 63 characters as this value
is stored in Kubernetes labels and there is limited length as mentioned
in https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
maxLength: 63
type: string
drupalVersion:
description: DrupalVersion defines the version of the Drupal to install
minLength: 1
......@@ -119,10 +125,6 @@ spec:
- type
type: object
type: array
phase:
description: 'Phase aggregates the information from all the conditions
and reports on the lifecycle phase of the resource Enum: {Creating,Created,Deleted}'
type: string
type: object
type: object
served: true
......
......@@ -93,8 +93,6 @@ func (r *DrupalSiteReconciler) Reconcile(ctx context.Context, req ctrl.Request)
//Handle deletion
if drupalSite.GetDeletionTimestamp() != nil {
// drupalSite.Status.Phase = "Deleted"
// r.updateCRStatusorFailReconcile(ctx, log, drupalSite)
return r.cleanupDrupalSite(ctx, log, drupalSite)
}
......@@ -110,7 +108,7 @@ func (r *DrupalSiteReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}
// Init. Check if finalizer is set. If not, set it, validate and update CR status
if update := ensureSpecFinalizer(drupalSite); update {
if update := ensureSpecFinalizer(drupalSite, log); update {
log.Info("Initializing DrupalSite Spec")
return r.updateCRorFailReconcile(ctx, log, drupalSite)
}
......
......@@ -18,8 +18,12 @@ package controllers
import (
"context"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"strings"
"time"
"github.com/asaskevich/govalidator"
"github.com/go-logr/logr"
......@@ -30,7 +34,6 @@ import (
webservicesv1a1 "gitlab.cern.ch/drupal/paas/drupalsite-operator/api/v1alpha1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
......@@ -45,6 +48,7 @@ const (
// finalizerStr string that is going to added to every DrupalSite created
finalizerStr = "controller.drupalsite.webservices.cern.ch"
productionEnvironment = "production"
routerShardLabel = "drupal.cern.ch/router-shard"
)
var (
......@@ -57,8 +61,21 @@ var (
ImageRecipesRepoRef string
// ClusterName is used in the Route's Host field
ClusterName string
// RouterShards is the list of all router shards available in the cluster - passed as a flag to the operator
RouterShards strFlagList
)
type strFlagList []string
func (i *strFlagList) String() string {
return strings.Join(*i, ",")
}
func (i *strFlagList) Set(value string) error {
*i = append(*i, value)
return nil
}
//validateSpec validates the spec against the DrupalSiteSpec definition
func validateSpec(drpSpec webservicesv1a1.DrupalSiteSpec) reconcileError {
_, err := govalidator.ValidateStruct(drpSpec)
......@@ -79,14 +96,38 @@ func contains(a []string, x string) bool {
// ensureSpecFinalizer ensures that the spec is valid, adding extra info if necessary, and that the finalizer is there,
// then returns if it needs to be updated.
func ensureSpecFinalizer(drp *webservicesv1a1.DrupalSite) (update bool) {
func ensureSpecFinalizer(drp *webservicesv1a1.DrupalSite, log logr.Logger) (update bool) {
if !contains(drp.GetFinalizers(), finalizerStr) {
drp.SetFinalizers(append(drp.GetFinalizers(), finalizerStr))
update = true
}
update, specErr := assignRouterShard(drp)
switch {
case specErr != nil:
log.Info(specErr.Error())
case update:
log.Info(fmt.Sprintf("Assigned router shard %v to a drupal site", drp.Spec.AssignedRouterShard))
}
return
}
func assignRouterShard(drp *webservicesv1a1.DrupalSite) (update bool, err reconcileError) {
if drp.Spec.AssignedRouterShard != "" {
return false, nil
}
if len(RouterShards) == 0 {
return false, newApplicationError(errors.New("specify a valid list of available router shards with the --assignable-router-shard flag"), ErrInvalidSpec)
}
drp.Spec.AssignedRouterShard = randomStrElement(RouterShards)
return true, nil
}
// randomStrElement returns a random element from the array. Panic if len(list) == 0
func randomStrElement(list []string) string {
r := rand.New(rand.NewSource(time.Now().Unix()))
return list[r.Intn(len(list))]
}
/*
ensureResources ensures the presence of all the resources that the DrupalSite needs to serve content, apart from the ingress Route.
This includes BuildConfigs/ImageStreams, DB, PVC, PHP/Nginx deployment + service, site install job.
......@@ -741,17 +782,17 @@ func serviceForDrupalSiteMySQL(d *webservicesv1a1.DrupalSite) *corev1.Service {
// routeForDrupalSite returns a route object
func routeForDrupalSite(d *webservicesv1a1.DrupalSite) *routev1.Route {
// ls := labelsForDrupalSite(d.Name)
var env string
if d.Spec.Environment.Name == productionEnvironment {
env = ""
} else {
labels := labelsForDrupalSite(d.Name)
labels[routerShardLabel] = d.Spec.AssignedRouterShard
env := ""
if d.Spec.Environment.Name != productionEnvironment {
env = d.Spec.Environment.Name + "."
}
route := &routev1.Route{
ObjectMeta: metav1.ObjectMeta{
Name: "drupal" + d.Name,
Namespace: d.Namespace,
Labels: labels,
},
Spec: routev1.RouteSpec{
Host: env + d.Name + "." + ClusterName + ".cern.ch",
......@@ -914,7 +955,7 @@ func createResource(ctx context.Context, res client.Object, name string, namespa
return r.Create(ctx, res)
}
err := r.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, res)
if err != nil && errors.IsNotFound(err) {
if err != nil && apierrors.IsNotFound(err) {
log.Info("Creating Resource", "Resource.Namespace", namespace, "Resource.Name", name)
err := r.Create(ctx, res)
if err != nil {
......
......@@ -71,12 +71,11 @@ func main() {
Development: true,
}
opts.BindFlags(flag.CommandLine)
initEnv()
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
initEnv()
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
......@@ -129,6 +128,7 @@ func initEnv() {
controllers.ImageRecipesRepoRef = "master"
}
controllers.ClusterName = getenvOrDie("CLUSTER_NAME")
flag.Var(&controllers.RouterShards, "assignable-router-shard", "List of available router shards")
}
func getenvOrDie(name string) string {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment