Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(STONEINTG-524): Report result of handling ITS to github in a new controller #296

Merged
merged 1 commit into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions controllers/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/redhat-appstudio/integration-service/controllers/integrationpipeline"
"github.com/redhat-appstudio/integration-service/controllers/scenario"
"github.com/redhat-appstudio/integration-service/controllers/snapshot"
"github.com/redhat-appstudio/integration-service/controllers/statusreport"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
)
Expand All @@ -34,6 +35,7 @@ var setupFunctions = []func(manager.Manager, *logr.Logger) error{
snapshot.SetupController,
scenario.SetupController,
binding.SetupController,
statusreport.SetupController,
}

// SetupControllers invoke all SetupController functions defined in setupFunctions, setting all controllers up and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ func (r *MockStatusReporter) ReportStatus(client.Client, context.Context, *tekto
return r.ReportStatusError
}

func (a *MockStatusAdapter) GetReporters(pipelineRun *tektonv1beta1.PipelineRun) ([]status.Reporter, error) {
func (r *MockStatusReporter) ReportStatusForSnapshot(client.Client, context.Context, *helpers.IntegrationLogger, *applicationapiv1alpha1.Snapshot) error {
r.Called = true
return r.ReportStatusError
}

func (a *MockStatusAdapter) GetReporters(object client.Object) ([]status.Reporter, error) {
return []status.Reporter{a.Reporter}, a.GetReportersError
}

Expand Down
73 changes: 73 additions & 0 deletions controllers/statusreport/statusreport_adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
Copyright 2023.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package statusreport

import (
"context"

applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1"
"github.com/redhat-appstudio/integration-service/helpers"
"github.com/redhat-appstudio/integration-service/status"

"github.com/redhat-appstudio/integration-service/loader"
"github.com/redhat-appstudio/operator-toolkit/controller"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// Adapter holds the objects needed to reconcile a snapshot's test status report.
type Adapter struct {
snapshot *applicationapiv1alpha1.Snapshot
application *applicationapiv1alpha1.Application
logger helpers.IntegrationLogger
loader loader.ObjectLoader
client client.Client
context context.Context
status status.Status
}

// NewAdapter creates and returns an Adapter instance.
func NewAdapter(snapshot *applicationapiv1alpha1.Snapshot, application *applicationapiv1alpha1.Application, logger helpers.IntegrationLogger, loader loader.ObjectLoader, client client.Client,
context context.Context) *Adapter {
return &Adapter{
snapshot: snapshot,
application: application,
logger: logger,
loader: loader,
client: client,
context: context,
status: status.NewAdapter(logger.Logger, client),
}
}

// EnsureSnapshotTestStatusReported will ensure that integration test status including env provision and snapshotEnvironmentBinding error is reported to the git provider
// which (indirectly) triggered its execution.
func (a *Adapter) EnsureSnapshotTestStatusReported() (controller.OperationResult, error) {
reporters, err := a.status.GetReporters(a.snapshot)
if err != nil {
return controller.RequeueWithError(err)
}

for _, reporter := range reporters {
if err := reporter.ReportStatusForSnapshot(a.client, a.context, &a.logger, a.snapshot); err != nil {
a.logger.Error(err, "failed to report test status to github for snapshot",
"snapshot.Namespace", a.snapshot.Namespace, "snapshot.Name", a.snapshot.Name)
return controller.RequeueWithError(err)
}
}

return controller.ContinueProcessing()
}
169 changes: 169 additions & 0 deletions controllers/statusreport/statusreport_adapter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
Copyright 2023.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package statusreport

import (
"context"
"fmt"
"reflect"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1"
"github.com/redhat-appstudio/integration-service/loader"
"github.com/redhat-appstudio/integration-service/status"
tektonv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/redhat-appstudio/integration-service/gitops"
"github.com/redhat-appstudio/integration-service/helpers"
"k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type MockStatusAdapter struct {
Reporter *MockStatusReporter
GetReportersError error
}

type MockStatusReporter struct {
Called bool
ReportStatusError error
}

func (r *MockStatusReporter) ReportStatus(client.Client, context.Context, *tektonv1beta1.PipelineRun) error {
r.Called = true
return r.ReportStatusError
}

func (r *MockStatusReporter) ReportStatusForSnapshot(client.Client, context.Context, *helpers.IntegrationLogger, *applicationapiv1alpha1.Snapshot) error {
r.Called = true
r.ReportStatusError = nil
return r.ReportStatusError
}

func (a *MockStatusAdapter) GetReporters(object client.Object) ([]status.Reporter, error) {
return []status.Reporter{a.Reporter}, a.GetReportersError
}

var _ = Describe("Snapshot Adapter", Ordered, func() {
var (
adapter *Adapter
logger helpers.IntegrationLogger
statusAdapter *MockStatusAdapter
statusReporter *MockStatusReporter

hasApp *applicationapiv1alpha1.Application
hasSnapshot *applicationapiv1alpha1.Snapshot
)
const (
SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic"
sample_image = "quay.io/redhat-appstudio/sample-image"
sample_revision = "random-value"
)

BeforeAll(func() {
hasApp = &applicationapiv1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "application-sample",
Namespace: "default",
},
Spec: applicationapiv1alpha1.ApplicationSpec{
DisplayName: "application-sample",
Description: "This is an example application",
},
}
Expect(k8sClient.Create(ctx, hasApp)).Should(Succeed())

hasSnapshot = &applicationapiv1alpha1.Snapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "snapshot-sample",
Namespace: "default",
Labels: map[string]string{
gitops.SnapshotTypeLabel: "component",
gitops.SnapshotComponentLabel: "component-sample",
"build.appstudio.redhat.com/pipeline": "enterprise-contract",
gitops.PipelineAsCodeEventTypeLabel: "pull_request",
"pac.test.appstudio.openshift.io/url-org": "testorg",
"pac.test.appstudio.openshift.io/url-repository": "testrepo",
"pac.test.appstudio.openshift.io/sha": "testsha",
gitops.PipelineAsCodeGitProviderLabel: gitops.PipelineAsCodeGitHubProviderType,
},
Annotations: map[string]string{
gitops.PipelineAsCodeInstallationIDAnnotation: "123",
"build.appstudio.redhat.com/commit_sha": "6c65b2fcaea3e1a0a92476c8b5dc89e92a85f025",
"appstudio.redhat.com/updateComponentOnSuccess": "false",
gitops.SnapshotTestsStatusAnnotation: "[{\"scenario\":\"scenario-1\",\"status\":\"EnvironmentProvisionError\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"completionTime\":\"2023-07-26T17:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:49+02:00\",\"details\":\"Failed to find deploymentTargetClass with right provisioner for copy of existingEnvironment\"}]",
},
},
Spec: applicationapiv1alpha1.SnapshotSpec{
Application: hasApp.Name,
Components: []applicationapiv1alpha1.SnapshotComponent{
{
Name: "component-sample",
ContainerImage: sample_image,
Source: applicationapiv1alpha1.ComponentSource{
ComponentSourceUnion: applicationapiv1alpha1.ComponentSourceUnion{
GitSource: &applicationapiv1alpha1.GitSource{
Revision: sample_revision,
},
},
},
},
},
},
}
Expect(k8sClient.Create(ctx, hasSnapshot)).Should(Succeed())
})

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

When("adapter is created", func() {
It("can create a new Adapter instance", func() {
Expect(reflect.TypeOf(NewAdapter(hasSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient, ctx))).To(Equal(reflect.TypeOf(&Adapter{})))
})

It("ensures the statusResport is called", func() {
adapter = NewAdapter(hasSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient, ctx)
statusReporter = &MockStatusReporter{}
statusAdapter = &MockStatusAdapter{Reporter: statusReporter}
adapter.status = statusAdapter
adapter.context = loader.GetMockedContext(ctx, []loader.MockData{
{
ContextKey: loader.ApplicationContextKey,
Resource: hasApp,
},
{
ContextKey: loader.SnapshotContextKey,
Resource: hasSnapshot,
},
})
result, err := adapter.EnsureSnapshotTestStatusReported()
fmt.Fprintf(GinkgoWriter, "-------err: %v\n", err)
hongweiliu17 marked this conversation as resolved.
Show resolved Hide resolved
fmt.Fprintf(GinkgoWriter, "-------result: %v\n", result)
Expect(!result.CancelRequest && err == nil).To(BeTrue())
})
})

})
105 changes: 105 additions & 0 deletions controllers/statusreport/statusreport_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
Copyright 2023.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions andF
limitations under the License.
*/

package statusreport

import (
"context"

"github.com/go-logr/logr"
applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1"
"github.com/redhat-appstudio/integration-service/gitops"
"github.com/redhat-appstudio/integration-service/helpers"
"github.com/redhat-appstudio/integration-service/loader"
"github.com/redhat-appstudio/operator-toolkit/controller"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// Reconciler reconciles an Snapshot object
type Reconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
}

// NewStatusReportReconciler creates and returns a Reconciler.
func NewStatusReportReconciler(client client.Client, logger *logr.Logger, scheme *runtime.Scheme) *Reconciler {
return &Reconciler{
Client: client,
Log: logger.WithName("statusreport"),
Scheme: scheme,
}
}

//+kubebuilder:rbac:groups=appstudio.redhat.com,resources=snapshots,verbs=get;list;watch
//+kubebuilder:rbac:groups=appstudio.redhat.com,resources=snapshots/status,verbs=get
//+kubebuilder:rbac:groups=appstudio.redhat.com,resources=applications,verbs=get;list;watch
//+kubebuilder:rbac:groups=appstudio.redhat.com,resources=applications/status,verbs=get

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := helpers.IntegrationLogger{Logger: r.Log.WithValues("snapshot", req.NamespacedName)}
loader := loader.NewLoader()

logger.Info("start to process snapshot test status since there is change in annotation test.appstudio.openshift.io/status", "snapshot", req.NamespacedName)

snapshot := &applicationapiv1alpha1.Snapshot{}
err := r.Get(ctx, req.NamespacedName, snapshot)
if err != nil {
logger.Error(err, "Failed to get snapshot for", "req", req.NamespacedName)
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}

return ctrl.Result{}, err
}

application, err := loader.GetApplicationFromSnapshot(r.Client, ctx, snapshot)
if err != nil {
logger.Error(err, "Failed to get Application from the Snapshot")
return ctrl.Result{}, err
}
logger = logger.WithApp(*application)

adapter := NewAdapter(snapshot, application, logger, loader, r.Client, ctx)
return controller.ReconcileHandler([]controller.Operation{
adapter.EnsureSnapshotTestStatusReported,
})
}

// AdapterInterface is an interface defining all the operations that should be defined in an Integration adapter.
type AdapterInterface interface {
EnsureSnapshotTestStatusReported() (controller.OperationResult, error)
}

// SetupController creates a new Integration controller and adds it to the Manager.
func SetupController(manager ctrl.Manager, log *logr.Logger) error {
return setupControllerWithManager(manager, NewStatusReportReconciler(manager.GetClient(), log, manager.GetScheme()))
}

// setupControllerWithManager sets up the controller with the Manager which monitors new Snapshots
func setupControllerWithManager(manager ctrl.Manager, controller *Reconciler) error {
return ctrl.NewControllerManagedBy(manager).
For(&applicationapiv1alpha1.Snapshot{}).
WithEventFilter(predicate.Or(
gitops.PRSnapshotTestAnnotationChangePredicate())).
Complete(controller)
}
Loading
Loading