controllers/jiraissuemetadata/jiraissuemetadata_controller.go (196 lines of code) (raw):
package jiraissuemetadata
import (
"context"
"fmt"
"os"
"reflect"
"time"
"github.com/go-logr/logr"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1"
"github.com/epam/edp-codebase-operator/v2/controllers/jiraissuemetadata/chain"
"github.com/epam/edp-codebase-operator/v2/pkg/client/jira"
"github.com/epam/edp-codebase-operator/v2/pkg/client/jira/dto"
codebasepredicate "github.com/epam/edp-codebase-operator/v2/pkg/predicate"
"github.com/epam/edp-codebase-operator/v2/pkg/util"
)
const (
reconcilePeriod = "RECONCILATION_PERIOD"
defaultReconcilePeriod = "360"
codebaseKind = "Codebase"
errorStatus = "error"
)
func NewReconcileJiraIssueMetadata(c client.Client, scheme *runtime.Scheme, log logr.Logger) *ReconcileJiraIssueMetadata {
return &ReconcileJiraIssueMetadata{
client: c,
scheme: scheme,
log: log.WithName("jira-issue-metadata"),
}
}
type ReconcileJiraIssueMetadata struct {
client client.Client
scheme *runtime.Scheme
log logr.Logger
}
func (r *ReconcileJiraIssueMetadata) SetupWithManager(mgr ctrl.Manager) error {
p := predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
oo, ok := e.ObjectOld.(*codebaseApi.JiraIssueMetadata)
if !ok {
return false
}
no, ok := e.ObjectNew.(*codebaseApi.JiraIssueMetadata)
if !ok {
return false
}
if codebasepredicate.PauseAnnotationChanged(oo, no) {
return true
}
return !reflect.DeepEqual(oo.Spec, no.Spec)
},
}
pause := codebasepredicate.NewPause(r.log)
err := ctrl.NewControllerManagedBy(mgr).
For(&codebaseApi.JiraIssueMetadata{}, builder.WithPredicates(pause, p)).
Complete(r)
if err != nil {
return fmt.Errorf("failed to build JiraIssueMetadata controller: %w", err)
}
return nil
}
//+kubebuilder:rbac:groups=v2.edp.epam.com,resources=jiraissuemetadatas,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=v2.edp.epam.com,resources=jiraissuemetadatas/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=v2.edp.epam.com,resources=jiraissuemetadatas/finalizers,verbs=update
// Reconcile reads that state of the cluster for a JiraIssueMetadata object and makes changes based on the state.
func (r *ReconcileJiraIssueMetadata) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
log := ctrl.LoggerFrom(ctx)
log.Info("Reconciling JiraIssueMetadata")
i := &codebaseApi.JiraIssueMetadata{}
if err := r.client.Get(ctx, request.NamespacedName, i); err != nil {
if k8sErrors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return reconcile.Result{}, fmt.Errorf("failed to fetch JiraIssueMetadata resource %q: %w", request.NamespacedName, err)
}
defer r.updateStatus(ctx, i)
if err := r.setOwnerRef(ctx, i); err != nil {
setErrorStatus(i, err.Error())
return reconcile.Result{}, err
}
js, err := r.getJiraServer(ctx, i)
if err != nil {
setErrorStatus(i, err.Error())
return reconcile.Result{}, err
}
if !js.Status.Available {
log.Info("Waiting for Jira server become available.", "name", js.Name)
return reconcile.Result{RequeueAfter: r.setFailureCount(i)}, nil
}
jc, err := r.initJiraClient(js)
if err != nil {
setErrorStatus(i, err.Error())
return reconcile.Result{}, err
}
ch, err := chain.CreateChain(i.Spec.Payload, jc, r.client)
if err != nil {
setErrorStatus(i, err.Error())
return reconcile.Result{}, fmt.Errorf("failed to configure `CreateChain`: %w", err)
}
err = ch.ServeRequest(ctx, i)
if err != nil {
setErrorStatus(i, err.Error())
timeout := r.setFailureCount(i)
log.Error(err, "failed to set jira issue metadata", "name", i.Name)
return reconcile.Result{RequeueAfter: timeout}, nil
}
duration, err := time.ParseDuration(lookup() + "m")
if err != nil {
return reconcile.Result{}, fmt.Errorf("failed to parse time duration: %w", err)
}
return reconcile.Result{RequeueAfter: duration}, nil
}
func lookup() string {
if value, ok := os.LookupEnv(reconcilePeriod); ok {
return value
}
return defaultReconcilePeriod
}
// setFailureCount increments failure count and returns delay for next reconciliation.
func (r *ReconcileJiraIssueMetadata) setFailureCount(metadata *codebaseApi.JiraIssueMetadata) time.Duration {
const timeoutDurationStep = 500 * time.Millisecond
timeout := util.GetTimeout(metadata.Status.FailureCount, timeoutDurationStep)
r.log.V(2).Info("wait for next reconciliation", "next reconciliation in", timeout)
metadata.Status.FailureCount++
return timeout
}
func (r *ReconcileJiraIssueMetadata) setOwnerRef(ctx context.Context, metadata *codebaseApi.JiraIssueMetadata) error {
c := &codebaseApi.Codebase{}
err := r.client.Get(ctx, types.NamespacedName{
Namespace: metadata.Namespace,
Name: metadata.Spec.CodebaseName,
}, c)
if err != nil {
return fmt.Errorf("failed to fetch Codebase resource %q: %w", metadata.Spec.CodebaseName, err)
}
if err := controllerutil.SetControllerReference(c, metadata, r.scheme); err != nil {
return fmt.Errorf("failed to set owner ref for JiraIssueMetadata CR: %w", err)
}
return nil
}
func setErrorStatus(metadata *codebaseApi.JiraIssueMetadata, msg string) {
metadata.Status.Status = errorStatus
metadata.Status.DetailedMessage = msg
}
func (r *ReconcileJiraIssueMetadata) updateStatus(ctx context.Context, instance *codebaseApi.JiraIssueMetadata) {
instance.Status.LastTimeUpdated = metaV1.Now()
err := r.client.Status().Update(ctx, instance)
if err != nil {
_ = r.client.Update(ctx, instance)
}
}
func (r *ReconcileJiraIssueMetadata) initJiraClient(js *codebaseApi.JiraServer) (jira.Client, error) {
s, err := util.GetSecret(r.client, js.Spec.CredentialName, js.Namespace)
if err != nil {
return nil, fmt.Errorf("failed to get secret %v: %w", js.Spec.CredentialName, err)
}
user := string(s.Data["username"])
pwd := string(s.Data["password"])
c, err := new(jira.GoJiraAdapterFactory).New(dto.ConvertSpecToJiraServer(js.Spec.ApiUrl, user, pwd))
if err != nil {
return nil, fmt.Errorf("failed to create Jira client: %w", err)
}
return c, nil
}
func (r *ReconcileJiraIssueMetadata) getJiraServer(ctx context.Context, metadata *codebaseApi.JiraIssueMetadata) (*codebaseApi.JiraServer, error) {
ref, err := util.GetOwnerReference(codebaseKind, metadata.GetOwnerReferences())
if err != nil {
return nil, fmt.Errorf("failed to fetch OwnerReference: %w", err)
}
c := &codebaseApi.Codebase{}
err = r.client.Get(ctx, types.NamespacedName{
Namespace: metadata.Namespace,
Name: ref.Name,
}, c)
if err != nil {
return nil, fmt.Errorf("failed to fetch Codebase resource %q: %w", ref.Name, err)
}
if c.Spec.JiraServer == nil {
return nil, fmt.Errorf("codebase %v has disabled jira integration. skip JiraIssueMetadata %v reconcilation",
c.Name, metadata.Name)
}
jiraServerName := *c.Spec.JiraServer
server := &codebaseApi.JiraServer{}
err = r.client.Get(ctx, types.NamespacedName{
Namespace: metadata.Namespace,
Name: jiraServerName,
}, server)
if err != nil {
return nil, fmt.Errorf("failed to fetch JiraServer resource %q: %w", jiraServerName, err)
}
return server, nil
}