Skip to content

Commit

Permalink
feat(RHTAPREL-253): queue Releases
Browse files Browse the repository at this point in the history
Signed-off-by: David Moreno García <[email protected]>
  • Loading branch information
davidmogar committed Apr 3, 2024
1 parent 2d4ad40 commit b435a02
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 3 deletions.
3 changes: 3 additions & 0 deletions api/v1alpha1/release_conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const (
// ProgressingReason is the reason set when an action is progressing
ProgressingReason conditions.ConditionReason = "Progressing"

// QueuedReason is the reason set when the action is queued
QueuedReason conditions.ConditionReason = "Queued"

// SucceededReason is the reason set when an action succeeds
SucceededReason conditions.ConditionReason = "Succeeded"
)
18 changes: 17 additions & 1 deletion api/v1alpha1/release_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,12 @@ func (r *Release) IsReleasing() bool {
return r.isPhaseProgressing(releasedConditionType)
}

// IsReleaseQueued checks whether the Release is queued.
func (r *Release) IsReleaseQueued() bool {
condition := meta.FindStatusCondition(r.Status.Conditions, releasedConditionType.String())
return condition != nil && condition.Status == metav1.ConditionFalse && condition.Reason == QueuedReason.String()
}

// IsValid checks whether the Release validation has finished successfully.
func (r *Release) IsValid() bool {
return meta.IsStatusConditionTrue(r.Status.Conditions, validatedConditionType.String())
Expand Down Expand Up @@ -383,6 +389,15 @@ func (r *Release) MarkReleaseFailed(message string) {
)
}

// MarkReleaseQueued marks the Release as queued.
func (r *Release) MarkReleaseQueued(message string) {
if !r.IsReleasing() || r.HasReleaseFinished() {
return
}

conditions.SetConditionWithMessage(&r.Status.Conditions, releasedConditionType, metav1.ConditionFalse, QueuedReason, message)
}

// MarkValidated marks the Release as validated.
func (r *Release) MarkValidated() {
if r.IsValid() {
Expand Down Expand Up @@ -454,7 +469,8 @@ func (r *Release) hasPhaseFinished(conditionType conditions.ConditionType) bool
case condition.Status == metav1.ConditionTrue:
return true
default:
return condition.Status == metav1.ConditionFalse && condition.Reason != ProgressingReason.String()
return condition.Status == metav1.ConditionFalse && condition.Reason != ProgressingReason.String() &&
condition.Reason != QueuedReason.String()
}
}

Expand Down
75 changes: 74 additions & 1 deletion api/v1alpha1/release_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,38 @@ var _ = Describe("Release type", func() {
})
})

When("IsReleaseQueued method is called", func() {
var release *Release

BeforeEach(func() {
release = &Release{}
})

It("should return false when the queued condition is missing", func() {
Expect(release.IsReleaseQueued()).To(BeFalse())
})

It("should return false when the released condition status is True", func() {
conditions.SetCondition(&release.Status.Conditions, releasedConditionType, metav1.ConditionTrue, QueuedReason)
Expect(release.IsReleaseQueued()).To(BeFalse())
})

It("should return true when the released condition status is False and the reason is Queued", func() {
conditions.SetCondition(&release.Status.Conditions, releasedConditionType, metav1.ConditionFalse, QueuedReason)
Expect(release.IsReleaseQueued()).To(BeTrue())
})

It("should return false when the released condition status is False and the reason is not Queued", func() {
conditions.SetCondition(&release.Status.Conditions, releasedConditionType, metav1.ConditionFalse, FailedReason)
Expect(release.IsReleaseQueued()).To(BeFalse())
})

It("should return false when the released condition status is Unknown", func() {
conditions.SetCondition(&release.Status.Conditions, releasedConditionType, metav1.ConditionUnknown, QueuedReason)
Expect(release.IsReleaseQueued()).To(BeFalse())
})
})

When("IsValid method is called", func() {
var release *Release

Expand Down Expand Up @@ -756,6 +788,42 @@ var _ = Describe("Release type", func() {
})
})

When("MarkReleaseQueued method is called", func() {
var release *Release

BeforeEach(func() {
release = &Release{}
})

It("should do nothing if the Release has not started", func() {
release.MarkReleaseQueued("")
Expect(release.Status.CompletionTime).To(BeNil())
})

It("should do nothing if the Release has finished", func() {
release.MarkReleasing("")
release.MarkReleased()
Expect(release.Status.CompletionTime.IsZero()).To(BeFalse())
release.Status.CompletionTime = &metav1.Time{}
release.MarkReleaseQueued("")
Expect(release.Status.CompletionTime.IsZero()).To(BeTrue())
})

It("should register the condition", func() {
Expect(release.Status.Conditions).To(HaveLen(0))
release.MarkReleasing("")
release.MarkReleaseQueued("foo")

condition := meta.FindStatusCondition(release.Status.Conditions, releasedConditionType.String())
Expect(condition).NotTo(BeNil())
Expect(*condition).To(MatchFields(IgnoreExtras, Fields{
"Message": Equal("foo"),
"Reason": Equal(QueuedReason.String()),
"Status": Equal(metav1.ConditionFalse),
}))
})
})

When("MarkValidated method is called", func() {
var release *Release

Expand Down Expand Up @@ -879,7 +947,12 @@ var _ = Describe("Release type", func() {
Expect(release.hasPhaseFinished(deployedConditionType)).To(BeFalse())
})

It("should return true when the condition status is False and the reason is not Progressing", func() {
It("should return false when the condition status is False and the reason is Queued", func() {
conditions.SetCondition(&release.Status.Conditions, deployedConditionType, metav1.ConditionFalse, QueuedReason)
Expect(release.hasPhaseFinished(deployedConditionType)).To(BeFalse())
})

It("should return true when the condition status is False and the reason is not Progressing nor Queued", func() {
conditions.SetCondition(&release.Status.Conditions, deployedConditionType, metav1.ConditionFalse, FailedReason)
Expect(release.hasPhaseFinished(deployedConditionType)).To(BeTrue())
})
Expand Down
16 changes: 16 additions & 0 deletions controllers/release/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,22 @@ func (a *adapter) EnsureReleaseIsProcessed() (controller.OperationResult, error)
return controller.ContinueProcessing()
}

pipelineRuns, err := a.loader.GetActiveManagedReleasePipelineRuns(a.ctx, a.client, a.release)
if err != nil {
return controller.RequeueWithError(err)
}

// Requeue the Release if a PipelineRun is already running for the same Application
if len(pipelineRuns.Items) > 0 {
patch := client.MergeFrom(a.release.DeepCopy())
a.release.MarkReleaseQueued(fmt.Sprintf("%d Release PipelineRun(s) running for the same Application", len(pipelineRuns.Items)))
err := a.client.Status().Patch(a.ctx, a.release, patch)
if err != nil {
return controller.RequeueWithError(err)
}
return controller.RequeueAfter(time.Minute, nil)
}

pipelineRun, err := a.loader.GetManagedReleasePipelineRun(a.ctx, a.client, a.release)
if err != nil && !errors.IsNotFound(err) {
return controller.RequeueWithError(err)
Expand Down
38 changes: 38 additions & 0 deletions controllers/release/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,17 @@ var _ = Describe("Release adapter", Ordered, func() {
Expect(adapter.release.IsReleasing()).To(BeTrue())
})

It("should mark the Release as releasing even if it is queued", func() {
adapter.release.MarkReleasing("")
adapter.release.MarkReleaseQueued("")
Expect(adapter.release.IsReleaseQueued()).To(BeTrue())
Expect(adapter.release.IsReleasing()).To(BeFalse())
result, err := adapter.EnsureReleaseIsRunning()
Expect(!result.RequeueRequest && !result.CancelRequest).To(BeTrue())
Expect(err).NotTo(HaveOccurred())
Expect(adapter.release.IsReleasing()).To(BeTrue())
})

It("should do nothing if the release is already running", func() {
adapter.release.MarkReleasing("")

Expand Down Expand Up @@ -351,6 +362,33 @@ var _ = Describe("Release adapter", Ordered, func() {
Expect(err).NotTo(HaveOccurred())
})

It("should queue the Release if another PipelineRun is running for the same Application", func() {
adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{
{
ContextKey: loader.ActiveManagedReleasePipelineRunsContextKey,
Resource: &tektonv1.PipelineRunList{
Items: []tektonv1.PipelineRun{
{
ObjectMeta: metav1.ObjectMeta{
Name: "pipeline-run",
Namespace: "default",
},
},
},
},
},
})

adapter.release.MarkReleasing("")
result, err := adapter.EnsureReleaseIsProcessed()
Expect(result.RequeueRequest && !result.CancelRequest).To(BeTrue())
Expect(result.RequeueDelay).To(Equal(time.Minute))
Expect(err).NotTo(HaveOccurred())
Expect(adapter.release.IsProcessing()).To(BeFalse())
Expect(adapter.release.IsReleaseQueued()).To(BeTrue())

})

It("should register the processing data if the PipelineRun already exists", func() {
adapter.ctx = toolkit.GetMockedContext(ctx, []toolkit.MockData{
{
Expand Down
27 changes: 27 additions & 0 deletions loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
)

type ObjectLoader interface {
GetActiveManagedReleasePipelineRuns(ctx context.Context, cli client.Client, release *v1alpha1.Release) (*tektonv1.PipelineRunList, error)
GetActiveReleasePlanAdmission(ctx context.Context, cli client.Client, releasePlan *v1alpha1.ReleasePlan) (*v1alpha1.ReleasePlanAdmission, error)
GetActiveReleasePlanAdmissionFromRelease(ctx context.Context, cli client.Client, release *v1alpha1.Release) (*v1alpha1.ReleasePlanAdmission, error)
GetApplication(ctx context.Context, cli client.Client, releasePlan *v1alpha1.ReleasePlan) (*applicationapiv1alpha1.Application, error)
Expand All @@ -44,6 +45,32 @@ func NewLoader() ObjectLoader {
return &loader{}
}

// GetActiveManagedReleasePipelineRuns returns all active managed Release PipelineRuns for the Application being Released.
// PipelineRuns for the Release passed as an argument are ignored.
func (l *loader) GetActiveManagedReleasePipelineRuns(ctx context.Context, cli client.Client, release *v1alpha1.Release) (*tektonv1.PipelineRunList, error) {
releasePlan, err := l.GetReleasePlan(ctx, cli, release)
if err != nil {
return nil, err
}

pipelineRuns := &tektonv1.PipelineRunList{}
err = cli.List(ctx, pipelineRuns,

Check failure on line 57 in loader/loader.go

View workflow job for this annotation

GitHub Actions / Check sources

this value of err is never used (SA4006)
client.InNamespace(releasePlan.Spec.Target),
client.MatchingLabels{
metadata.ApplicationNameLabel: releasePlan.Spec.Application,
})

for i := len(pipelineRuns.Items) - 1; i >= 0; i-- {
releaseName, _ := pipelineRuns.Items[i].Labels[metadata.ReleaseNameLabel]

Check failure on line 64 in loader/loader.go

View workflow job for this annotation

GitHub Actions / Check sources

unnecessary assignment to the blank identifier (S1005)
if releaseName == release.Name || pipelineRuns.Items[i].IsDone() {
// Remove completed PipelineRuns or PipelineRuns triggered for the Release passed as argument
pipelineRuns.Items = append(pipelineRuns.Items[:i], pipelineRuns.Items[i+1:]...)
}
}

return pipelineRuns, nil
}

// GetActiveReleasePlanAdmission returns the ReleasePlanAdmission targeted by the given ReleasePlan.
// Only ReleasePlanAdmissions with the 'auto-release' label set to true (or missing the label, which is
// treated the same as having the label and it being set to true) will be searched for. If a matching
Expand Down
10 changes: 9 additions & 1 deletion loader/loader_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import (
)

const (
ApplicationComponentsContextKey toolkit.ContextKey = iota
ActiveManagedReleasePipelineRunsContextKey toolkit.ContextKey = iota
ApplicationComponentsContextKey
ApplicationContextKey
EnterpriseContractConfigMapContextKey
EnterpriseContractPolicyContextKey
Expand All @@ -41,6 +42,13 @@ func NewMockLoader() ObjectLoader {
}
}

func (l *mockLoader) GetActiveManagedReleasePipelineRuns(ctx context.Context, cli client.Client, release *v1alpha1.Release) (*tektonv1.PipelineRunList, error) {
if ctx.Value(ActiveManagedReleasePipelineRunsContextKey) == nil {
return l.loader.GetActiveManagedReleasePipelineRuns(ctx, cli, release)
}
return toolkit.GetMockedResourceAndErrorFromContext(ctx, ActiveManagedReleasePipelineRunsContextKey, &tektonv1.PipelineRunList{})
}

// GetActiveReleasePlanAdmission returns the resource and error passed as values of the context.
func (l *mockLoader) GetActiveReleasePlanAdmission(ctx context.Context, cli client.Client, releasePlan *v1alpha1.ReleasePlan) (*v1alpha1.ReleasePlanAdmission, error) {
if ctx.Value(ReleasePlanAdmissionContextKey) == nil {
Expand Down
15 changes: 15 additions & 0 deletions loader/loader_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ var _ = Describe("Release Adapter", Ordered, func() {
loader = NewMockLoader()
})

When("calling GetActiveManagedReleasePipelineRuns", func() {
It("returns the resource and error from the context", func() {
pipelineRuns := &tektonv1.PipelineRunList{}
mockContext := toolkit.GetMockedContext(ctx, []toolkit.MockData{
{
ContextKey: ActiveManagedReleasePipelineRunsContextKey,
Resource: pipelineRuns,
},
})
resource, err := loader.GetActiveManagedReleasePipelineRuns(mockContext, nil, nil)
Expect(resource).To(Equal(pipelineRuns))
Expect(err).To(BeNil())
})
})

When("calling GetActiveReleasePlanAdmission", func() {
It("returns the resource and error from the context", func() {
releasePlanAdmission := &v1alpha1.ReleasePlanAdmission{}
Expand Down
29 changes: 29 additions & 0 deletions loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,34 @@ var _ = Describe("Release Adapter", Ordered, func() {
loader = NewLoader()
})

When("calling GetActiveManagedReleasePipelineRuns", func() {
var pipelineRunOne, pipelineRunTwo *tektonv1.PipelineRun

BeforeEach(func() {
pipelineRunOne = pipelineRun.DeepCopy()
pipelineRunOne.Labels[metadata.ReleaseNameLabel] = "foo"
pipelineRunOne.Name = "pr-one"
pipelineRunOne.ResourceVersion = ""
pipelineRunTwo = pipelineRun.DeepCopy()
pipelineRunTwo.Name = "pr-two"
pipelineRunTwo.ResourceVersion = ""
Expect(k8sClient.Create(ctx, pipelineRunOne)).To(Succeed())
Expect(k8sClient.Create(ctx, pipelineRunTwo)).To(Succeed())
})

AfterEach(func() {
Expect(k8sClient.Delete(ctx, pipelineRunOne)).To(Succeed())
Expect(k8sClient.Delete(ctx, pipelineRunTwo)).To(Succeed())
})

It("returns the requested list of PipelineRuns", func() {
Eventually(func() bool {
returnedObject, err := loader.GetActiveManagedReleasePipelineRuns(ctx, k8sClient, release)
return returnedObject != &tektonv1.PipelineRunList{} && err == nil && len(returnedObject.Items) == 1
})
})
})

When("calling GetActiveReleasePlanAdmission", func() {
It("returns an active release plan admission", func() {
returnedObject, err := loader.GetActiveReleasePlanAdmission(ctx, k8sClient, releasePlan)
Expand Down Expand Up @@ -475,6 +503,7 @@ var _ = Describe("Release Adapter", Ordered, func() {
pipelineRun = &tektonv1.PipelineRun{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
metadata.ApplicationNameLabel: releasePlan.Spec.Application,
metadata.ReleaseNameLabel: release.Name,
metadata.ReleaseNamespaceLabel: release.Namespace,
},
Expand Down

0 comments on commit b435a02

Please sign in to comment.