Skip to content

Commit

Permalink
Merge pull request #277 from MartinBasti/report-test-status
Browse files Browse the repository at this point in the history
feat(STONEINTG-523): Report status of integration tests into snapshot
  • Loading branch information
MartinBasti authored Sep 19, 2023
2 parents b2a1c26 + f70eed2 commit 3aa8f0e
Show file tree
Hide file tree
Showing 13 changed files with 470 additions and 148 deletions.
45 changes: 37 additions & 8 deletions controllers/binding/snapshotenvironmentbinding_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
)

const SnapshotEnvironmentBindingErrorTimeoutSeconds float64 = 300

// Adapter holds the objects needed to reconcile a SnapshotEnvironmentBinding.
type Adapter struct {
snapshotEnvironmentBinding *applicationapiv1alpha1.SnapshotEnvironmentBinding
Expand Down Expand Up @@ -128,20 +130,32 @@ func (a *Adapter) EnsureEphemeralEnvironmentsCleanedUp() (controller.OperationRe
// reasonable time then we assume that the SEB is stuck in an unrecoverable
// state and clean it up. Otherwise we requeue and wait until the timeout
// has passed
const snapshotEnvironmentBindingErrorTimeoutSeconds float64 = 300
var lastTransitionTime time.Time
bindingStatus := meta.FindStatusCondition(a.snapshotEnvironmentBinding.Status.BindingConditions, gitops.BindingErrorOccurredStatusConditionType)
if bindingStatus != nil {
lastTransitionTime = bindingStatus.LastTransitionTime.Time
}
sinceLastTransition := time.Since(lastTransitionTime).Seconds()
if sinceLastTransition < snapshotEnvironmentBindingErrorTimeoutSeconds {
if sinceLastTransition < SnapshotEnvironmentBindingErrorTimeoutSeconds {
// don't log here, it floods logs
return controller.RequeueAfter(time.Duration(snapshotEnvironmentBindingErrorTimeoutSeconds*float64(time.Second)), nil)
} else {
a.logger.Info(
fmt.Sprintf("SEB has been in the error state for more than the threshold time of %f seconds and will be deleted", snapshotEnvironmentBindingErrorTimeoutSeconds),
)
return controller.RequeueAfter(time.Duration(SnapshotEnvironmentBindingErrorTimeoutSeconds*float64(time.Second)), nil)
}

reasonMsg := "Unknown reason"

if conditionStatus := gitops.GetBindingConditionStatus(a.snapshotEnvironmentBinding); conditionStatus != nil {
reasonMsg = fmt.Sprintf("%s (%s)", conditionStatus.Reason, conditionStatus.Message)
}
// we don't want to scare users prematurely, report error only after SEB timeout for trying to deploy
a.logger.Info(
fmt.Sprintf("SEB has been in the error state for more than the threshold time of %f seconds", SnapshotEnvironmentBindingErrorTimeoutSeconds),
"reason", reasonMsg,
)

err := a.writeTestStatusIntoSnapshot(gitops.IntegrationTestStatusDeploymentError,
fmt.Sprintf("The SnapshotEnvironmentBinding has failed to deploy on ephemeral environment: %s", reasonMsg))
if err != nil {
return controller.RequeueWithError(fmt.Errorf("failed to update snapshot test status: %w", err))
}

// mark snapshot as failed
Expand All @@ -150,7 +164,7 @@ func (a *Adapter) EnsureEphemeralEnvironmentsCleanedUp() (controller.OperationRe
a.logger.Info("The SnapshotEnvironmentBinding encountered an issue deploying snapshot on ephemeral environments",
"snapshotEnvironmentBinding.Name", a.snapshotEnvironmentBinding.Name,
"message", snapshotErrorMessage)
_, err := gitops.MarkSnapshotAsFailed(a.client, a.context, a.snapshot, snapshotErrorMessage)
_, err = gitops.MarkSnapshotAsFailed(a.client, a.context, a.snapshot, snapshotErrorMessage)
if err != nil {
a.logger.Error(err, "Failed to Update Snapshot status")
return controller.RequeueWithError(err)
Expand Down Expand Up @@ -220,3 +234,18 @@ func (a *Adapter) getDeploymentTargetForEnvironment(environment *applicationapiv

return deploymentTarget, nil
}

// writeTestStatusIntoSnapshot updates test status and instantly writes changes into test result annotation
func (a *Adapter) writeTestStatusIntoSnapshot(status gitops.IntegrationTestStatus, details string) error {
testStatuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(a.snapshot)
if err != nil {
return err
}
testStatuses.UpdateTestStatusIfChanged(a.integrationTestScenario.Name, status, details)
err = gitops.WriteIntegrationTestStatusesIntoSnapshot(a.snapshot, testStatuses, a.client, a.context)
if err != nil {
return err

}
return nil
}
62 changes: 62 additions & 0 deletions controllers/binding/snapshotenvironmentbinding_adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,4 +528,66 @@ var _ = Describe("Binding Adapter", Ordered, func() {
environments, _ := adapter.loader.GetAllEnvironments(k8sClient, adapter.context, hasApp)
Expect(*environments).To(ContainElement(HaveField("ObjectMeta.Name", "envname")))
})

When("binding deployment failed", func() {
var (
failedBinding *applicationapiv1alpha1.SnapshotEnvironmentBinding
)

BeforeEach(func() {
failedBinding = &applicationapiv1alpha1.SnapshotEnvironmentBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "snapshot-binding-sample-failed",
Namespace: "default",
Labels: map[string]string{
gitops.SnapshotTestScenarioLabel: integrationTestScenario.Name,
},
},
Spec: applicationapiv1alpha1.SnapshotEnvironmentBindingSpec{
Application: hasApp.Name,
Snapshot: hasSnapshot.Name,
Environment: hasEnv.Name,
Components: []applicationapiv1alpha1.BindingComponent{},
},
}
Expect(k8sClient.Create(ctx, failedBinding)).Should(Succeed())

failedBinding.Status = applicationapiv1alpha1.SnapshotEnvironmentBindingStatus{
BindingConditions: []metav1.Condition{
{
Reason: "Failed",
Status: "True",
Type: gitops.BindingErrorOccurredStatusConditionType,
LastTransitionTime: metav1.Time{
// time set after timeout so cleanup is called
Time: time.Now().Add(time.Duration(-1*SnapshotEnvironmentBindingErrorTimeoutSeconds*float64(time.Second) + 1)),
},
},
},
}
Expect(k8sClient.Status().Update(ctx, failedBinding)).Should(Succeed())
})

AfterEach(func() {
err := k8sClient.Delete(ctx, failedBinding)
Expect(err == nil || errors.IsNotFound(err)).To(BeTrue())
})

It("Failed binding test status is reported into snapshot", func() {
adapter = NewAdapter(failedBinding, hasSnapshot, hasEnv, hasApp, hasComp, integrationTestScenario, logger, loader.NewMockLoader(), k8sClient, ctx)
Expect(reflect.TypeOf(adapter)).To(Equal(reflect.TypeOf(&Adapter{})))

Eventually(func() bool {
result, err := adapter.EnsureEphemeralEnvironmentsCleanedUp()
return !result.CancelRequest && err == nil
}, time.Second*20).Should(BeTrue())

statuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(hasSnapshot)
Expect(err).To(BeNil())
detail, ok := statuses.GetScenarioStatus(integrationTestScenario.Name)
Expect(ok).To(BeTrue())
Expect(detail.Status).To(Equal(gitops.IntegrationTestStatusDeploymentError))
})

})
})
55 changes: 55 additions & 0 deletions controllers/integrationpipeline/integrationpipeline_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/redhat-appstudio/operator-toolkit/metadata"
tektonv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
)

Expand Down Expand Up @@ -190,6 +191,41 @@ func (a *Adapter) EnsureStatusReported() (controller.OperationResult, error) {
return controller.ContinueProcessing()
}

// EnsureStatusReportedInSnapshot will ensure that status of the integration test pipelines is reported to snapshot
// to be consumed by user
func (a *Adapter) EnsureStatusReportedInSnapshot() (controller.OperationResult, error) {

// pipelines run in parallel and have great potential to cause conflict on update
// thus `RetryOnConflict` is easy solution here, given the snaphost must be loaded specifically here
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {

snapshot, err := a.loader.GetSnapshotFromPipelineRun(a.client, a.context, a.pipelineRun)
if err != nil {
return err
}

statuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(snapshot)
if err != nil {
return err
}

status, detail, err := GetIntegrationPipelineRunStatus(a.client, a.context, a.pipelineRun)
if err != nil {
return err
}
statuses.UpdateTestStatusIfChanged(a.pipelineRun.Labels[tekton.ScenarioNameLabel], status, detail)

// don't return wrapped err for retries
err = gitops.WriteIntegrationTestStatusesIntoSnapshot(snapshot, statuses, a.client, a.context)
return err
})
if err != nil {
a.logger.Error(err, "Failed to update pipeline status in snapshot")
return controller.RequeueWithError(fmt.Errorf("failed to update test status in snapshot: %w", err))
}
return controller.ContinueProcessing()
}

// EnsureEphemeralEnvironmentsCleanedUp will ensure that ephemeral environment(s) associated with the
// integration PipelineRun are cleaned up.
func (a *Adapter) EnsureEphemeralEnvironmentsCleanedUp() (controller.OperationResult, error) {
Expand Down Expand Up @@ -399,3 +435,22 @@ func (a *Adapter) createCompositeSnapshotsIfConflictExists(application *applicat

return nil, nil
}

// GetIntegrationPipelineRunStatus checks the Tekton results for a given PipelineRun and returns status of test.
func GetIntegrationPipelineRunStatus(adapterClient client.Client, ctx context.Context, pipelineRun *tektonv1beta1.PipelineRun) (gitops.IntegrationTestStatus, string, error) {
// Check if the pipelineRun finished from the condition of status
if !h.HasPipelineRunFinished(pipelineRun) {
return gitops.IntegrationTestStatusInProgress, fmt.Sprintf("Integration test is running as pipeline run '%s'", pipelineRun.Name), nil
}

outcome, err := h.GetIntegrationPipelineRunOutcome(adapterClient, ctx, pipelineRun)
if err != nil {
return gitops.IntegrationTestStatusTestFail, "", fmt.Errorf("failed to evaluate inegration test results: %w", err)
}

if !outcome.HasPipelineRunPassedTesting() {
return gitops.IntegrationTestStatusTestFail, "Integration test failed", nil
}

return gitops.IntegrationTestStatusTestPassed, "Integration test passed", nil
}
Loading

0 comments on commit 3aa8f0e

Please sign in to comment.