From 0e2673968237215b83ccbdb5320576c8f0d50d5b Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 8 Dec 2023 17:44:10 -0800 Subject: [PATCH 01/39] test(scorecard): scorecard tests for recording management Signed-off-by: Thuan Vo --- config/rbac/oauth_client.yaml | 1 - .../rbac/scorecard_role.yaml | 49 +++ internal/test/scorecard/common_utils.go | 288 +++++++++++++++ internal/test/scorecard/openshift.go | 7 + internal/test/scorecard/tests.go | 328 ++---------------- 5 files changed, 380 insertions(+), 293 deletions(-) create mode 100644 internal/test/scorecard/common_utils.go diff --git a/config/rbac/oauth_client.yaml b/config/rbac/oauth_client.yaml index d8c6693c..b1c50771 100644 --- a/config/rbac/oauth_client.yaml +++ b/config/rbac/oauth_client.yaml @@ -3,7 +3,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: oauth-client rules: - apiGroups: diff --git a/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml b/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml index de76f86d..867d6f58 100644 --- a/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml +++ b/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml @@ -94,3 +94,52 @@ rules: - namespaces verbs: - create +--- +# Permissions for default OAuth configurations +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: oauth-client +rules: +- apiGroups: + - operator.cryostat.io + resources: + - cryostats + verbs: + - create + - patch + - delete + - get +- apiGroups: + - "" + resources: + - pods + - pods/exec + - services + verbs: + - create + - patch + - delete + - get +- apiGroups: + - "" + resources: + - replicationcontrollers + - endpoints + verbs: + - get +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - get +- apiGroups: + - apps + resources: + - daemonsets + - replicasets + - statefulsets + verbs: + - get diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go new file mode 100644 index 00000000..ae41fd98 --- /dev/null +++ b/internal/test/scorecard/common_utils.go @@ -0,0 +1,288 @@ +package scorecard + +import ( + "context" + "fmt" + "time" + + operatorv1beta1 "github.com/cryostatio/cryostat-operator/api/v1beta1" + scapiv1alpha3 "github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes/scheme" +) + +const ( + operatorDeploymentName string = "cryostat-operator-controller-manager" + cryostastCRName string = "cryostat-cr-test" + testTimeout time.Duration = time.Minute * 10 +) + +func waitForDeploymentAvailability(ctx context.Context, client *CryostatClientset, namespace string, + name string, r *scapiv1alpha3.TestResult) error { + err := wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { + deploy, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + if kerrors.IsNotFound(err) { + r.Log += fmt.Sprintf("deployment %s is not yet found\n", name) + return false, nil // Retry + } + return false, fmt.Errorf("failed to get deployment: %s", err.Error()) + } + // Check for Available condition + for _, condition := range deploy.Status.Conditions { + if condition.Type == appsv1.DeploymentAvailable && + condition.Status == corev1.ConditionTrue { + r.Log += fmt.Sprintf("deployment %s is available\n", deploy.Name) + return true, nil + } + if condition.Type == appsv1.DeploymentReplicaFailure && + condition.Status == corev1.ConditionTrue { + r.Log += fmt.Sprintf("deployment %s is failing, %s: %s\n", deploy.Name, + condition.Reason, condition.Message) + } + } + r.Log += fmt.Sprintf("deployment %s is not yet available\n", deploy.Name) + return false, nil + }) + if err != nil { + logErr := logErrors(r, client, namespace, name) + if logErr != nil { + r.Log += fmt.Sprintf("failed to look up deployment errors: %s\n", logErr.Error()) + } + } + return err +} + +func fail(r scapiv1alpha3.TestResult, message string) scapiv1alpha3.TestResult { + r.State = scapiv1alpha3.FailState + r.Errors = append(r.Errors, message) + return r +} + +func logErrors(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace string, name string) error { + ctx := context.Background() + deploy, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return err + } + // Log deployment conditions and events + r.Log += fmt.Sprintf("deployment %s conditions:\n", deploy.Name) + for _, condition := range deploy.Status.Conditions { + r.Log += fmt.Sprintf("\t%s == %s, %s: %s\n", condition.Type, + condition.Status, condition.Reason, condition.Message) + } + + r.Log += fmt.Sprintf("deployment %s warning events:\n", deploy.Name) + err = logEvents(r, client, namespace, scheme.Scheme, deploy) + if err != nil { + return err + } + + // Look up replica sets for deployment and log conditions and events + selector, err := metav1.LabelSelectorAsSelector(deploy.Spec.Selector) + if err != nil { + return err + } + replicaSets, err := client.AppsV1().ReplicaSets(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: selector.String(), + }) + if err != nil { + return err + } + for _, rs := range replicaSets.Items { + r.Log += fmt.Sprintf("replica set %s conditions:\n", rs.Name) + for _, condition := range rs.Status.Conditions { + r.Log += fmt.Sprintf("\t%s == %s, %s: %s\n", condition.Type, condition.Status, + condition.Reason, condition.Message) + } + r.Log += fmt.Sprintf("replica set %s warning events:\n", rs.Name) + err = logEvents(r, client, namespace, scheme.Scheme, &rs) + if err != nil { + return err + } + } + + // Look up pods for deployment and log conditions and events + pods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: selector.String(), + }) + if err != nil { + return err + } + for _, pod := range pods.Items { + r.Log += fmt.Sprintf("pod %s phase: %s\n", pod.Name, pod.Status.Phase) + r.Log += fmt.Sprintf("pod %s conditions:\n", pod.Name) + for _, condition := range pod.Status.Conditions { + r.Log += fmt.Sprintf("\t%s == %s, %s: %s\n", condition.Type, condition.Status, + condition.Reason, condition.Message) + } + r.Log += fmt.Sprintf("pod %s warning events:\n", pod.Name) + err = logEvents(r, client, namespace, scheme.Scheme, &pod) + if err != nil { + return err + } + } + return nil +} + +func logEvents(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace string, + scheme *runtime.Scheme, obj runtime.Object) error { + events, err := client.CoreV1().Events(namespace).Search(scheme, obj) + if err != nil { + return err + } + for _, event := range events.Items { + if event.Type == corev1.EventTypeWarning { + r.Log += fmt.Sprintf("\t%s: %s\n", event.Reason, event.Message) + } + } + return nil +} + +func newEmptyTestResult(testName string) scapiv1alpha3.TestResult { + return scapiv1alpha3.TestResult{ + Name: testName, + State: scapiv1alpha3.PassState, + Errors: make([]string, 0), + Suggestions: make([]string, 0), + } +} + +func newCryostatCR(namespace string, withIngress bool) *operatorv1beta1.Cryostat { + cr := &operatorv1beta1.Cryostat{ + ObjectMeta: metav1.ObjectMeta{ + Name: cryostastCRName, + Namespace: namespace, + }, + Spec: operatorv1beta1.CryostatSpec{ + Minimal: false, + EnableCertManager: &[]bool{true}[0], + }, + } + + if withIngress { + pathType := netv1.PathTypePrefix + cr.Spec.NetworkOptions = &operatorv1beta1.NetworkConfigurationList{ + CoreConfig: &operatorv1beta1.NetworkConfiguration{ + Annotations: map[string]string{ + "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS", + }, + IngressSpec: &netv1.IngressSpec{ + TLS: []netv1.IngressTLS{{}}, + Rules: []netv1.IngressRule{ + { + Host: "testing.cryostat", + IngressRuleValue: netv1.IngressRuleValue{ + HTTP: &netv1.HTTPIngressRuleValue{ + Paths: []netv1.HTTPIngressPath{ + { + Path: "/", + PathType: &pathType, + Backend: netv1.IngressBackend{ + Service: &netv1.IngressServiceBackend{ + Name: cryostastCRName, + Port: netv1.ServiceBackendPort{ + Number: 8181, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + GrafanaConfig: &operatorv1beta1.NetworkConfiguration{ + Annotations: map[string]string{ + "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS", + }, + IngressSpec: &netv1.IngressSpec{ + TLS: []netv1.IngressTLS{{}}, + Rules: []netv1.IngressRule{ + { + Host: "testing.cryostat-grafana", + IngressRuleValue: netv1.IngressRuleValue{ + HTTP: &netv1.HTTPIngressRuleValue{ + Paths: []netv1.HTTPIngressPath{ + { + Path: "/", + PathType: &pathType, + Backend: netv1.IngressBackend{ + Service: &netv1.IngressServiceBackend{ + Name: fmt.Sprintf("%s-grafana", cryostastCRName), + Port: netv1.ServiceBackendPort{ + Number: 3000, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + } + return cr +} + +func createAndWaitForCryostat(cr *operatorv1beta1.Cryostat, client *CryostatClientset, r scapiv1alpha3.TestResult) scapiv1alpha3.TestResult { + ctx := context.Background() + cr, err := client.OperatorCRDs().Cryostats(cr.Namespace).Create(ctx, cr) + if err != nil { + return fail(r, fmt.Sprintf("failed to create Cryostat CR: %s", err.Error())) + } + + // Poll the deployment until it becomes available or we timeout + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + err = waitForDeploymentAvailability(ctx, client, cr.Namespace, cr.Name, &r) + if err != nil { + return fail(r, fmt.Sprintf("Cryostat main deployment did not become available: %s", err.Error())) + } + + err = wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { + cr, err = client.OperatorCRDs().Cryostats(cr.Namespace).Get(ctx, cr.Name) + if err != nil { + return false, fmt.Errorf("failed to get Cryostat CR: %s", err.Error()) + } + if len(cr.Status.ApplicationURL) > 0 { + return true, nil + } + r.Log += "Application URL is not yet available\n" + return false, nil + }) + if err != nil { + return fail(r, fmt.Sprintf("Application URL not found in CR: %s", err.Error())) + } + r.Log += fmt.Sprintf("Application is ready at %s\n", cr.Status.ApplicationURL) + + return r +} + +func cleanupCryostat(r scapiv1alpha3.TestResult, client *CryostatClientset, namespace string) scapiv1alpha3.TestResult { + cr := &operatorv1beta1.Cryostat{ + ObjectMeta: metav1.ObjectMeta{ + Name: cryostastCRName, + Namespace: namespace, + }, + } + ctx := context.Background() + err := client.OperatorCRDs().Cryostats(cr.Namespace).Delete(ctx, + cr.Name, &metav1.DeleteOptions{}) + if err != nil { + r.Log += fmt.Sprintf("failed to delete Cryostat: %s\n", err.Error()) + } + return r +} diff --git a/internal/test/scorecard/openshift.go b/internal/test/scorecard/openshift.go index dee12c4d..247068a4 100644 --- a/internal/test/scorecard/openshift.go +++ b/internal/test/scorecard/openshift.go @@ -27,12 +27,15 @@ import ( ctrl "sigs.k8s.io/controller-runtime" configv1 "github.com/openshift/api/config/v1" + routev1 "github.com/openshift/api/route/v1" corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/discovery" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" ) @@ -202,3 +205,7 @@ func installOpenShiftCertManager(r *scapiv1alpha3.TestResult) error { return false, nil }) } + +func isOpenShift(client discovery.DiscoveryInterface) (bool, error) { + return discovery.IsResourceEnabled(client, routev1.GroupVersion.WithResource("routes")) +} diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index b2aab776..eccd927d 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -17,38 +17,36 @@ package scorecard import ( "context" "fmt" - "time" - operatorv1beta1 "github.com/cryostatio/cryostat-operator/api/v1beta1" scapiv1alpha3 "github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3" apimanifests "github.com/operator-framework/api/pkg/manifests" - - routev1 "github.com/openshift/api/route/v1" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - netv1 "k8s.io/api/networking/v1" - kerrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/discovery" - "k8s.io/client-go/kubernetes/scheme" ) const ( - OperatorInstallTestName string = "operator-install" - CryostatCRTestName string = "cryostat-cr" - operatorDeploymentName string = "cryostat-operator-controller-manager" - testTimeout time.Duration = time.Minute * 10 + OperatorInstallTestName string = "operator-install" + CryostatCRTestName string = "cryostat-cr" + CryostatRecordingTestName string = "cryostat-recording" ) +func commonCRTestSetup(testName string, openShiftCertManager bool) (*CryostatClientset, scapiv1alpha3.TestResult) { + r := newEmptyTestResult(testName) + // Create a new Kubernetes REST client for this test + client, err := NewClientset() + if err != nil { + return nil, fail(r, fmt.Sprintf("failed to create client: %s", err.Error())) + } + if openShiftCertManager { + err := installOpenShiftCertManager(&r) + if err != nil { + return client, fail(r, fmt.Sprintf("failed to install cert-manager Operator for Red Hat OpenShift: %s", err.Error())) + } + } + return client, r +} + // OperatorInstallTest checks that the operator installed correctly func OperatorInstallTest(bundle *apimanifests.Bundle, namespace string) scapiv1alpha3.TestResult { - r := scapiv1alpha3.TestResult{} - r.Name = OperatorInstallTestName - r.State = scapiv1alpha3.PassState - r.Errors = make([]string, 0) - r.Suggestions = make([]string, 0) + r := newEmptyTestResult(OperatorInstallTestName) // Create a new Kubernetes REST client for this test client, err := NewClientset() @@ -69,286 +67,32 @@ func OperatorInstallTest(bundle *apimanifests.Bundle, namespace string) scapiv1a // CryostatCRTest checks that the operator installs Cryostat in response to a Cryostat CR func CryostatCRTest(bundle *apimanifests.Bundle, namespace string, openShiftCertManager bool) scapiv1alpha3.TestResult { - r := scapiv1alpha3.TestResult{} - r.Name = CryostatCRTestName - r.State = scapiv1alpha3.PassState - r.Errors = make([]string, 0) - r.Suggestions = make([]string, 0) - - // Create a new Kubernetes REST client for this test - client, err := NewClientset() - if err != nil { - return fail(r, fmt.Sprintf("failed to create client: %s", err.Error())) + client, r := commonCRTestSetup(CryostatCRTestName, openShiftCertManager) + if r.State != scapiv1alpha3.PassState { + return r } - defer cleanupCryostat(&r, client, namespace) - - openshift, err := isOpenShift(client.DiscoveryClient) + openshift, err := isOpenShift(client) if err != nil { return fail(r, fmt.Sprintf("could not determine whether platform is OpenShift: %s", err.Error())) } - - if openshift && openShiftCertManager { - err := installOpenShiftCertManager(&r) - if err != nil { - return fail(r, fmt.Sprintf("failed to install cert-manager Operator for Red Hat OpenShift: %s", err.Error())) - } - } - // Create a default Cryostat CR - cr := newCryostatCR(namespace, !openshift) - - ctx := context.Background() - cr, err = client.OperatorCRDs().Cryostats(namespace).Create(ctx, cr) - if err != nil { - return fail(r, fmt.Sprintf("failed to create Cryostat CR: %s", err.Error())) - } - - // Poll the deployment until it becomes available or we timeout - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - err = waitForDeploymentAvailability(ctx, client, cr.Namespace, cr.Name, &r) - if err != nil { - return fail(r, fmt.Sprintf("Cryostat main deployment did not become available: %s", err.Error())) - } - - err = wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { - cr, err = client.OperatorCRDs().Cryostats(namespace).Get(ctx, cr.Name) - if err != nil { - return false, fmt.Errorf("failed to get Cryostat CR: %s", err.Error()) - } - if len(cr.Status.ApplicationURL) > 0 { - return true, nil - } - r.Log += "Application URL is not yet available\n" - return false, nil - }) - if err != nil { - return fail(r, fmt.Sprintf("Application URL not found in CR: %s", err.Error())) - } - r.Log += fmt.Sprintf("Application is ready at %s\n", cr.Status.ApplicationURL) - - return r + r = createAndWaitForCryostat(newCryostatCR(namespace, !openshift), client, r) + return cleanupCryostat(r, client, namespace) } -func waitForDeploymentAvailability(ctx context.Context, client *CryostatClientset, namespace string, - name string, r *scapiv1alpha3.TestResult) error { - err := wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { - deploy, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - if kerrors.IsNotFound(err) { - r.Log += fmt.Sprintf("deployment %s is not yet found\n", name) - return false, nil // Retry - } - return false, fmt.Errorf("failed to get deployment: %s", err.Error()) - } - // Check for Available condition - for _, condition := range deploy.Status.Conditions { - if condition.Type == appsv1.DeploymentAvailable && - condition.Status == corev1.ConditionTrue { - r.Log += fmt.Sprintf("deployment %s is available\n", deploy.Name) - return true, nil - } - if condition.Type == appsv1.DeploymentReplicaFailure && - condition.Status == corev1.ConditionTrue { - r.Log += fmt.Sprintf("deployment %s is failing, %s: %s\n", deploy.Name, - condition.Reason, condition.Message) - } - } - r.Log += fmt.Sprintf("deployment %s is not yet available\n", deploy.Name) - return false, nil - }) - if err != nil { - logErr := logErrors(r, client, namespace, name) - if logErr != nil { - r.Log += fmt.Sprintf("failed to look up deployment errors: %s\n", logErr.Error()) - } +func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openShiftCertManager bool) scapiv1alpha3.TestResult { + client, r := commonCRTestSetup(CryostatRecordingTestName, openShiftCertManager) + if r.State != scapiv1alpha3.PassState { + return r } - return err -} - -func fail(r scapiv1alpha3.TestResult, message string) scapiv1alpha3.TestResult { - r.State = scapiv1alpha3.FailState - r.Errors = append(r.Errors, message) - return r -} - -func cleanupCryostat(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace string) { - cr := &operatorv1beta1.Cryostat{ - ObjectMeta: metav1.ObjectMeta{ - Name: "cryostat-cr-test", - Namespace: namespace, - }, - } - ctx := context.Background() - err := client.OperatorCRDs().Cryostats(cr.Namespace).Delete(ctx, - cr.Name, &metav1.DeleteOptions{}) - if err != nil { - r.Log += fmt.Sprintf("failed to delete Cryostat: %s\n", err.Error()) - } -} - -func logErrors(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace string, name string) error { - ctx := context.Background() - deploy, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return err - } - // Log deployment conditions and events - r.Log += fmt.Sprintf("deployment %s conditions:\n", deploy.Name) - for _, condition := range deploy.Status.Conditions { - r.Log += fmt.Sprintf("\t%s == %s, %s: %s\n", condition.Type, - condition.Status, condition.Reason, condition.Message) - } - - r.Log += fmt.Sprintf("deployment %s warning events:\n", deploy.Name) - err = logEvents(r, client, namespace, scheme.Scheme, deploy) - if err != nil { - return err - } - - // Look up replica sets for deployment and log conditions and events - selector, err := metav1.LabelSelectorAsSelector(deploy.Spec.Selector) - if err != nil { - return err - } - replicaSets, err := client.AppsV1().ReplicaSets(namespace).List(ctx, metav1.ListOptions{ - LabelSelector: selector.String(), - }) - if err != nil { - return err - } - for _, rs := range replicaSets.Items { - r.Log += fmt.Sprintf("replica set %s conditions:\n", rs.Name) - for _, condition := range rs.Status.Conditions { - r.Log += fmt.Sprintf("\t%s == %s, %s: %s\n", condition.Type, condition.Status, - condition.Reason, condition.Message) - } - r.Log += fmt.Sprintf("replica set %s warning events:\n", rs.Name) - err = logEvents(r, client, namespace, scheme.Scheme, &rs) - if err != nil { - return err - } - } - - // Look up pods for deployment and log conditions and events - pods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ - LabelSelector: selector.String(), - }) - if err != nil { - return err - } - for _, pod := range pods.Items { - r.Log += fmt.Sprintf("pod %s phase: %s\n", pod.Name, pod.Status.Phase) - r.Log += fmt.Sprintf("pod %s conditions:\n", pod.Name) - for _, condition := range pod.Status.Conditions { - r.Log += fmt.Sprintf("\t%s == %s, %s: %s\n", condition.Type, condition.Status, - condition.Reason, condition.Message) - } - r.Log += fmt.Sprintf("pod %s warning events:\n", pod.Name) - err = logEvents(r, client, namespace, scheme.Scheme, &pod) - if err != nil { - return err - } - } - return nil -} - -func logEvents(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace string, - scheme *runtime.Scheme, obj runtime.Object) error { - events, err := client.CoreV1().Events(namespace).Search(scheme, obj) + openshift, err := isOpenShift(client) if err != nil { - return err - } - for _, event := range events.Items { - if event.Type == corev1.EventTypeWarning { - r.Log += fmt.Sprintf("\t%s: %s\n", event.Reason, event.Message) - } - } - return nil -} - -func newCryostatCR(namespace string, withIngress bool) *operatorv1beta1.Cryostat { - cr := &operatorv1beta1.Cryostat{ - ObjectMeta: metav1.ObjectMeta{ - Name: "cryostat-cr-test", - Namespace: namespace, - }, - Spec: operatorv1beta1.CryostatSpec{ - Minimal: false, - EnableCertManager: &[]bool{true}[0], - }, + return fail(r, fmt.Sprintf("could not determine whether platform is OpenShift: %s", err.Error())) } + // Create a default Cryostat CR + r = createAndWaitForCryostat(newCryostatCR(namespace, !openshift), client, r) - if withIngress { - pathType := netv1.PathTypePrefix - cr.Spec.NetworkOptions = &operatorv1beta1.NetworkConfigurationList{ - CoreConfig: &operatorv1beta1.NetworkConfiguration{ - Annotations: map[string]string{ - "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS", - }, - IngressSpec: &netv1.IngressSpec{ - TLS: []netv1.IngressTLS{{}}, - Rules: []netv1.IngressRule{ - { - Host: "testing.cryostat", - IngressRuleValue: netv1.IngressRuleValue{ - HTTP: &netv1.HTTPIngressRuleValue{ - Paths: []netv1.HTTPIngressPath{ - { - Path: "/", - PathType: &pathType, - Backend: netv1.IngressBackend{ - Service: &netv1.IngressServiceBackend{ - Name: "cryostat-cr-test", - Port: netv1.ServiceBackendPort{ - Number: 8181, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - GrafanaConfig: &operatorv1beta1.NetworkConfiguration{ - Annotations: map[string]string{ - "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS", - }, - IngressSpec: &netv1.IngressSpec{ - TLS: []netv1.IngressTLS{{}}, - Rules: []netv1.IngressRule{ - { - Host: "testing.cryostat-grafana", - IngressRuleValue: netv1.IngressRuleValue{ - HTTP: &netv1.HTTPIngressRuleValue{ - Paths: []netv1.HTTPIngressPath{ - { - Path: "/", - PathType: &pathType, - Backend: netv1.IngressBackend{ - Service: &netv1.IngressServiceBackend{ - Name: "cryostat-cr-test-grafana", - Port: netv1.ServiceBackendPort{ - Number: 3000, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - } - return cr -} + // Create a recording -func isOpenShift(client discovery.DiscoveryInterface) (bool, error) { - return discovery.IsResourceEnabled(client, routev1.GroupVersion.WithResource("routes")) + return cleanupCryostat(r, client, namespace) } From 5003566d3d4c77680dee1c099196061b5a1ff79a Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 8 Dec 2023 18:03:34 -0800 Subject: [PATCH 02/39] fixup(scorecard): fix cr cleanup func --- internal/test/scorecard/common_utils.go | 3 +-- internal/test/scorecard/tests.go | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go index ae41fd98..951d72b9 100644 --- a/internal/test/scorecard/common_utils.go +++ b/internal/test/scorecard/common_utils.go @@ -271,7 +271,7 @@ func createAndWaitForCryostat(cr *operatorv1beta1.Cryostat, client *CryostatClie return r } -func cleanupCryostat(r scapiv1alpha3.TestResult, client *CryostatClientset, namespace string) scapiv1alpha3.TestResult { +func cleanupCryostat(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace string) { cr := &operatorv1beta1.Cryostat{ ObjectMeta: metav1.ObjectMeta{ Name: cryostastCRName, @@ -284,5 +284,4 @@ func cleanupCryostat(r scapiv1alpha3.TestResult, client *CryostatClientset, name if err != nil { r.Log += fmt.Sprintf("failed to delete Cryostat: %s\n", err.Error()) } - return r } diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index eccd927d..00bf9b80 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -76,8 +76,10 @@ func CryostatCRTest(bundle *apimanifests.Bundle, namespace string, openShiftCert return fail(r, fmt.Sprintf("could not determine whether platform is OpenShift: %s", err.Error())) } // Create a default Cryostat CR - r = createAndWaitForCryostat(newCryostatCR(namespace, !openshift), client, r) - return cleanupCryostat(r, client, namespace) + cr := newCryostatCR(namespace, !openshift) + defer cleanupCryostat(&r, client, namespace) + + return createAndWaitForCryostat(cr, client, r) } func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openShiftCertManager bool) scapiv1alpha3.TestResult { @@ -90,9 +92,19 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh return fail(r, fmt.Sprintf("could not determine whether platform is OpenShift: %s", err.Error())) } // Create a default Cryostat CR - r = createAndWaitForCryostat(newCryostatCR(namespace, !openshift), client, r) + cr := newCryostatCR(namespace, !openshift) + defer cleanupCryostat(&r, client, namespace) + + r = createAndWaitForCryostat(cr, client, r) - // Create a recording + // // FIXME + // endpoint := fmt.Sprintf("/api/v1/targets/:targetId/recordings") + + // resp, err = http.Post(endpoint, "application/json", nil) + // if err != nil { + // return fail(r, fmt.Sprintf("could not determine whether platform is OpenShift: %s", err.Error())) + // } + // defer resp.Body.close() + return r - return cleanupCryostat(r, client, namespace) } From e9b932e85c81303428d6eb9dd6dfe1302b76f6ac Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 8 Dec 2023 18:24:11 -0800 Subject: [PATCH 03/39] test(scorecard): registry recording test to suite --- internal/images/custom-scorecard-tests/main.go | 3 +++ internal/test/scorecard/common_utils.go | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/internal/images/custom-scorecard-tests/main.go b/internal/images/custom-scorecard-tests/main.go index 310ea1ef..96684530 100644 --- a/internal/images/custom-scorecard-tests/main.go +++ b/internal/images/custom-scorecard-tests/main.go @@ -90,6 +90,7 @@ func validateTests(testNames []string) bool { switch testName { case tests.OperatorInstallTestName: case tests.CryostatCRTestName: + case tests.CryostatRecordingTestName: default: return false } @@ -108,6 +109,8 @@ func runTests(testNames []string, bundle *apimanifests.Bundle, namespace string, results = append(results, tests.OperatorInstallTest(bundle, namespace)) case tests.CryostatCRTestName: results = append(results, tests.CryostatCRTest(bundle, namespace, openShiftCertManager)) + case tests.CryostatRecordingTestName: + results = append(results, tests.CryostatRecordingTest(bundle, namespace, openShiftCertManager)) default: log.Fatalf("unknown test found: %s", testName) } diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go index 951d72b9..2254c63e 100644 --- a/internal/test/scorecard/common_utils.go +++ b/internal/test/scorecard/common_utils.go @@ -1,3 +1,17 @@ +// Copyright The Cryostat Authors. +// +// 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 scorecard import ( From 6cfefe1892906a8ff98b9375063b9f6de75aaaba Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 8 Dec 2023 19:59:47 -0800 Subject: [PATCH 04/39] chore(scorecard): reorganize client def --- internal/test/scorecard/clients.go | 66 +++++++++++++++--------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 2cf994c3..d594ca6c 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -19,10 +19,10 @@ import ( operatorv1beta1 "github.com/cryostatio/cryostat-operator/api/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" @@ -35,6 +35,11 @@ type CryostatClientset struct { operatorClient *OperatorCRDClient } +// OperatorCRDs returns a OperatorCRDClient +func (c *CryostatClientset) OperatorCRDs() *OperatorCRDClient { + return c.operatorClient +} + // NewClientset creates a CryostatClientset func NewClientset() (*CryostatClientset, error) { // Get in-cluster REST config from pod @@ -60,9 +65,19 @@ func NewClientset() (*CryostatClientset, error) { }, nil } -// OperatorCRDs returns a OperatorCRDClient -func (c *CryostatClientset) OperatorCRDs() *OperatorCRDClient { - return c.operatorClient +// OperatorCRDClient is a Kubernetes REST client for performing operations on +// Cryostat Operator custom resources +type OperatorCRDClient struct { + client *rest.RESTClient +} + +// Cryostats returns a CryostatClient configured to a specific namespace +func (c *OperatorCRDClient) Cryostats(namespace string) *CryostatClient { + return &CryostatClient{ + restClient: c.client, + namespace: namespace, + resource: "cryostats", + } } func newOperatorCRDClient(config *rest.Config) (*OperatorCRDClient, error) { @@ -75,19 +90,21 @@ func newOperatorCRDClient(config *rest.Config) (*OperatorCRDClient, error) { }, nil } -// OperatorCRDClient is a Kubernetes REST client for performing operations on -// Cryostat Operator custom resources -type OperatorCRDClient struct { - client *rest.RESTClient +func newCRDClient(config *rest.Config) (*rest.RESTClient, error) { + scheme := runtime.NewScheme() + if err := operatorv1beta1.AddToScheme(scheme); err != nil { + return nil, err + } + return newRESTClientForGV(config, scheme, &operatorv1beta1.GroupVersion) } -// Cryostats returns a CryostatClient configured to a specific namespace -func (c *OperatorCRDClient) Cryostats(namespace string) *CryostatClient { - return &CryostatClient{ - restClient: c.client, - namespace: namespace, - resource: "cryostats", - } +func newRESTClientForGV(config *rest.Config, scheme *runtime.Scheme, gv *schema.GroupVersion) (*rest.RESTClient, error) { + configCopy := *config + configCopy.GroupVersion = gv + configCopy.APIPath = "/apis" + configCopy.ContentType = runtime.ContentTypeJSON + configCopy.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)} + return rest.RESTClientFor(&configCopy) } // CryostatClient contains methods to perform operations on @@ -118,23 +135,6 @@ func (c *CryostatClient) Delete(ctx context.Context, name string, options *metav return delete(ctx, c.restClient, c.resource, c.namespace, name, options) } -func newCRDClient(config *rest.Config) (*rest.RESTClient, error) { - scheme := runtime.NewScheme() - if err := operatorv1beta1.AddToScheme(scheme); err != nil { - return nil, err - } - return newRESTClientForGV(config, scheme, &operatorv1beta1.GroupVersion) -} - -func newRESTClientForGV(config *rest.Config, scheme *runtime.Scheme, gv *schema.GroupVersion) (*rest.RESTClient, error) { - configCopy := *config - configCopy.GroupVersion = gv - configCopy.APIPath = "/apis" - configCopy.ContentType = runtime.ContentTypeJSON - configCopy.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)} - return rest.RESTClientFor(&configCopy) -} - func get[r runtime.Object](ctx context.Context, c rest.Interface, res string, ns string, name string, result r) (r, error) { err := c.Get(). Namespace(ns).Resource(res). From 86e977fc65336e361435376b8486f9318496dc75 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 8 Dec 2023 23:44:37 -0800 Subject: [PATCH 05/39] chore(scorecard): clean up common setup func --- ...yostat-operator.clusterserviceversion.yaml | 2 +- bundle/tests/scorecard/config.yaml | 4 +- config/scorecard/patches/custom.config.yaml | 4 +- internal/test/scorecard/clients.go | 68 ++++++++++++++++ internal/test/scorecard/common_utils.go | 74 ++++++++++++++--- internal/test/scorecard/tests.go | 79 +++++++------------ 6 files changed, 165 insertions(+), 66 deletions(-) diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index e29650ba..e362c0d5 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -54,7 +54,7 @@ metadata: capabilities: Seamless Upgrades categories: Monitoring, Developer Tools containerImage: quay.io/cryostat/cryostat-operator:2.5.0-dev - createdAt: "2024-02-04T06:09:16Z" + createdAt: "2024-02-12T19:47:24Z" description: JVM monitoring and profiling tool operatorframework.io/initialization-resource: |- { diff --git a/bundle/tests/scorecard/config.yaml b/bundle/tests/scorecard/config.yaml index ba7284c7..1a3d864a 100644 --- a/bundle/tests/scorecard/config.yaml +++ b/bundle/tests/scorecard/config.yaml @@ -69,7 +69,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - operator-install - image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231011144522 + image: quay.io/thvo/cryostat-operator-scorecard:2.5.0-20231211070342 labels: suite: cryostat test: operator-install @@ -79,7 +79,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231011144522 + image: quay.io/thvo/cryostat-operator-scorecard:2.5.0-20231211070342 labels: suite: cryostat test: cryostat-cr diff --git a/config/scorecard/patches/custom.config.yaml b/config/scorecard/patches/custom.config.yaml index 786cf388..004cbd3b 100644 --- a/config/scorecard/patches/custom.config.yaml +++ b/config/scorecard/patches/custom.config.yaml @@ -8,7 +8,7 @@ entrypoint: - cryostat-scorecard-tests - operator-install - image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231011144522" + image: "quay.io/thvo/cryostat-operator-scorecard:2.5.0-20231211070342" labels: suite: cryostat test: operator-install @@ -18,7 +18,7 @@ entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231011144522" + image: "quay.io/thvo/cryostat-operator-scorecard:2.5.0-20231211070342" labels: suite: cryostat test: cryostat-cr diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index d594ca6c..e9f0a907 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -16,6 +16,8 @@ package scorecard import ( "context" + "net/http" + "net/url" operatorv1beta1 "github.com/cryostatio/cryostat-operator/api/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -162,3 +164,69 @@ func delete(ctx context.Context, c rest.Interface, res string, ns string, name s Name(name).Body(opts).Do(ctx). Error() } + +// CryostatRESTClientset contains methods to interact with +// the Cryostat API +type CryostatRESTClientset struct { + // Application URL pointing to Cryostat + base *url.URL + v1Client *APIV1Client +} + +func NewCryostatRESTClientset(applicationURL string) (*CryostatRESTClientset, error) { + base, err := url.Parse(applicationURL) + if err != nil { + return nil, err + } + return &CryostatRESTClientset{ + base: base, + v1Client: &APIV1Client{}, + }, nil +} + +func (cs *CryostatRESTClientset) APIV1(applicationURL string) *APIV1Client { + return cs.v1Client +} + +type TargetInterface interface { + List() []runtime.Object +} + +type RecordingInterface interface { + Get(target string, name string) []runtime.Object + Delete(target string, name string) error +} + +type CryostatAPIClient interface { + Version() string + Prefix() string +} + +type APIV1Client struct{} + +func (v1 *APIV1Client) Version() string { + return "v1" +} + +func (v1 *APIV1Client) Prefix() string { + return "/api/v1" +} + +// func (v1 *APIV1Client) Targets() TargetInterface { +// return &TargetInterface{ +// List() { + +// } +// } +// } + +// CryostatRESTRequest +type CryostatRESTRequest struct { + base *url.URL + verb string + headers http.Header + params url.Values + + APIversion string + path string +} diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go index 2254c63e..356ff921 100644 --- a/internal/test/scorecard/common_utils.go +++ b/internal/test/scorecard/common_utils.go @@ -38,6 +38,12 @@ const ( testTimeout time.Duration = time.Minute * 10 ) +type TestResources struct { + OpenShift bool + Client *CryostatClientset + *scapiv1alpha3.TestResult +} + func waitForDeploymentAvailability(ctx context.Context, client *CryostatClientset, namespace string, name string, r *scapiv1alpha3.TestResult) error { err := wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { @@ -66,7 +72,7 @@ func waitForDeploymentAvailability(ctx context.Context, client *CryostatClientse return false, nil }) if err != nil { - logErr := logErrors(r, client, namespace, name) + logErr := logWorkloadEvents(r, client, namespace, name) if logErr != nil { r.Log += fmt.Sprintf("failed to look up deployment errors: %s\n", logErr.Error()) } @@ -74,13 +80,18 @@ func waitForDeploymentAvailability(ctx context.Context, client *CryostatClientse return err } +func logError(r *scapiv1alpha3.TestResult, message string) { + r.State = scapiv1alpha3.FailState + r.Errors = append(r.Errors, message) +} + func fail(r scapiv1alpha3.TestResult, message string) scapiv1alpha3.TestResult { r.State = scapiv1alpha3.FailState r.Errors = append(r.Errors, message) return r } -func logErrors(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace string, name string) error { +func logWorkloadEvents(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace string, name string) error { ctx := context.Background() deploy, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { @@ -160,8 +171,8 @@ func logEvents(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace return nil } -func newEmptyTestResult(testName string) scapiv1alpha3.TestResult { - return scapiv1alpha3.TestResult{ +func newEmptyTestResult(testName string) *scapiv1alpha3.TestResult { + return &scapiv1alpha3.TestResult{ Name: testName, State: scapiv1alpha3.PassState, Errors: make([]string, 0), @@ -169,6 +180,40 @@ func newEmptyTestResult(testName string) scapiv1alpha3.TestResult { } } +func newTestResources(testName string) *TestResources { + return &TestResources{ + TestResult: newEmptyTestResult(testName), + } +} + +func setupCRTestResources(tr *TestResources, openShiftCertManager bool) error { + r := tr.TestResult + + // Create a new Kubernetes REST client for this test + client, err := NewClientset() + if err != nil { + logError(r, fmt.Sprintf("failed to create client: %s", err.Error())) + return err + } + tr.Client = client + + openshift, err := isOpenShift(client) + if err != nil { + logError(r, fmt.Sprintf("could not determine whether platform is OpenShift: %s", err.Error())) + return err + } + tr.OpenShift = openshift + + if openshift && openShiftCertManager { + err := installOpenShiftCertManager(r) + if err != nil { + logError(r, fmt.Sprintf("failed to install cert-manager Operator for Red Hat OpenShift: %s", err.Error())) + return err + } + } + return nil +} + func newCryostatCR(namespace string, withIngress bool) *operatorv1beta1.Cryostat { cr := &operatorv1beta1.Cryostat{ ObjectMeta: metav1.ObjectMeta{ @@ -251,19 +296,23 @@ func newCryostatCR(namespace string, withIngress bool) *operatorv1beta1.Cryostat return cr } -func createAndWaitForCryostat(cr *operatorv1beta1.Cryostat, client *CryostatClientset, r scapiv1alpha3.TestResult) scapiv1alpha3.TestResult { - ctx := context.Background() - cr, err := client.OperatorCRDs().Cryostats(cr.Namespace).Create(ctx, cr) +func createAndWaitForCryostat(cr *operatorv1beta1.Cryostat, resources *TestResources) error { + client := resources.Client + r := resources.TestResult + + cr, err := client.OperatorCRDs().Cryostats(cr.Namespace).Create(context.Background(), cr) if err != nil { - return fail(r, fmt.Sprintf("failed to create Cryostat CR: %s", err.Error())) + logError(r, fmt.Sprintf("failed to create Cryostat CR: %s", err.Error())) + return err } // Poll the deployment until it becomes available or we timeout ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - err = waitForDeploymentAvailability(ctx, client, cr.Namespace, cr.Name, &r) + err = waitForDeploymentAvailability(ctx, client, cr.Namespace, cr.Name, r) if err != nil { - return fail(r, fmt.Sprintf("Cryostat main deployment did not become available: %s", err.Error())) + logError(r, fmt.Sprintf("Cryostat main deployment did not become available: %s", err.Error())) + return err } err = wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { @@ -278,11 +327,12 @@ func createAndWaitForCryostat(cr *operatorv1beta1.Cryostat, client *CryostatClie return false, nil }) if err != nil { - return fail(r, fmt.Sprintf("Application URL not found in CR: %s", err.Error())) + logError(r, fmt.Sprintf("Application URL not found in CR: %s", err.Error())) + return err } r.Log += fmt.Sprintf("Application is ready at %s\n", cr.Status.ApplicationURL) - return r + return nil } func cleanupCryostat(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace string) { diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 00bf9b80..0d535f0d 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -28,22 +28,6 @@ const ( CryostatRecordingTestName string = "cryostat-recording" ) -func commonCRTestSetup(testName string, openShiftCertManager bool) (*CryostatClientset, scapiv1alpha3.TestResult) { - r := newEmptyTestResult(testName) - // Create a new Kubernetes REST client for this test - client, err := NewClientset() - if err != nil { - return nil, fail(r, fmt.Sprintf("failed to create client: %s", err.Error())) - } - if openShiftCertManager { - err := installOpenShiftCertManager(&r) - if err != nil { - return client, fail(r, fmt.Sprintf("failed to install cert-manager Operator for Red Hat OpenShift: %s", err.Error())) - } - } - return client, r -} - // OperatorInstallTest checks that the operator installed correctly func OperatorInstallTest(bundle *apimanifests.Bundle, namespace string) scapiv1alpha3.TestResult { r := newEmptyTestResult(OperatorInstallTestName) @@ -51,60 +35,57 @@ func OperatorInstallTest(bundle *apimanifests.Bundle, namespace string) scapiv1a // Create a new Kubernetes REST client for this test client, err := NewClientset() if err != nil { - return fail(r, fmt.Sprintf("failed to create client: %s", err.Error())) + return fail(*r, fmt.Sprintf("failed to create client: %s", err.Error())) } // Poll the deployment until it becomes available or we timeout ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() - err = waitForDeploymentAvailability(ctx, client, namespace, operatorDeploymentName, &r) + err = waitForDeploymentAvailability(ctx, client, namespace, operatorDeploymentName, r) if err != nil { - return fail(r, fmt.Sprintf("operator deployment did not become available: %s", err.Error())) + return fail(*r, fmt.Sprintf("operator deployment did not become available: %s", err.Error())) } - return r + return *r } // CryostatCRTest checks that the operator installs Cryostat in response to a Cryostat CR func CryostatCRTest(bundle *apimanifests.Bundle, namespace string, openShiftCertManager bool) scapiv1alpha3.TestResult { - client, r := commonCRTestSetup(CryostatCRTestName, openShiftCertManager) - if r.State != scapiv1alpha3.PassState { - return r - } - openshift, err := isOpenShift(client) + tr := newTestResources(CryostatCRTestName) + r := tr.TestResult + + err := setupCRTestResources(tr, openShiftCertManager) if err != nil { - return fail(r, fmt.Sprintf("could not determine whether platform is OpenShift: %s", err.Error())) + return fail(*r, fmt.Sprintf("failed to set up %s test: %s", CryostatCRTestName, err.Error())) } + // Create a default Cryostat CR - cr := newCryostatCR(namespace, !openshift) - defer cleanupCryostat(&r, client, namespace) + cr := newCryostatCR(namespace, !tr.OpenShift) + defer cleanupCryostat(r, tr.Client, namespace) - return createAndWaitForCryostat(cr, client, r) + err = createAndWaitForCryostat(cr, tr) + if err != nil { + return fail(*r, fmt.Sprintf("%s test failed: %s", CryostatCRTestName, err.Error())) + } + return *r } func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openShiftCertManager bool) scapiv1alpha3.TestResult { - client, r := commonCRTestSetup(CryostatRecordingTestName, openShiftCertManager) - if r.State != scapiv1alpha3.PassState { - return r - } - openshift, err := isOpenShift(client) + tr := newTestResources(CryostatCRTestName) + r := tr.TestResult + + err := setupCRTestResources(tr, openShiftCertManager) if err != nil { - return fail(r, fmt.Sprintf("could not determine whether platform is OpenShift: %s", err.Error())) + return fail(*r, fmt.Sprintf("failed to set up %s test: %s", CryostatRecordingTestName, err.Error())) } - // Create a default Cryostat CR - cr := newCryostatCR(namespace, !openshift) - defer cleanupCryostat(&r, client, namespace) - r = createAndWaitForCryostat(cr, client, r) - - // // FIXME - // endpoint := fmt.Sprintf("/api/v1/targets/:targetId/recordings") - - // resp, err = http.Post(endpoint, "application/json", nil) - // if err != nil { - // return fail(r, fmt.Sprintf("could not determine whether platform is OpenShift: %s", err.Error())) - // } - // defer resp.Body.close() - return r + // Create a default Cryostat CR + cr := newCryostatCR(namespace, !tr.OpenShift) + defer cleanupCryostat(r, tr.Client, namespace) + err = createAndWaitForCryostat(cr, tr) + if err != nil { + return fail(*r, fmt.Sprintf("%s test failed: %s", CryostatRecordingTestName, err.Error())) + } + return *r } From 543b6148177a0a8f73d3c6612296a4d39a0b456d Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Sun, 10 Dec 2023 23:29:44 -0800 Subject: [PATCH 06/39] chore(bundle): regenerate bundle with scorecard tag --- bundle/manifests/cryostat-operator.clusterserviceversion.yaml | 2 +- bundle/tests/scorecard/config.yaml | 4 ++-- config/scorecard/patches/custom.config.yaml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index e362c0d5..32039efc 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -54,7 +54,7 @@ metadata: capabilities: Seamless Upgrades categories: Monitoring, Developer Tools containerImage: quay.io/cryostat/cryostat-operator:2.5.0-dev - createdAt: "2024-02-12T19:47:24Z" + createdAt: "2024-02-12T19:48:04Z" description: JVM monitoring and profiling tool operatorframework.io/initialization-resource: |- { diff --git a/bundle/tests/scorecard/config.yaml b/bundle/tests/scorecard/config.yaml index 1a3d864a..9a353826 100644 --- a/bundle/tests/scorecard/config.yaml +++ b/bundle/tests/scorecard/config.yaml @@ -69,7 +69,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - operator-install - image: quay.io/thvo/cryostat-operator-scorecard:2.5.0-20231211070342 + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824 labels: suite: cryostat test: operator-install @@ -79,7 +79,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: quay.io/thvo/cryostat-operator-scorecard:2.5.0-20231211070342 + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824 labels: suite: cryostat test: cryostat-cr diff --git a/config/scorecard/patches/custom.config.yaml b/config/scorecard/patches/custom.config.yaml index 004cbd3b..011d7364 100644 --- a/config/scorecard/patches/custom.config.yaml +++ b/config/scorecard/patches/custom.config.yaml @@ -8,7 +8,7 @@ entrypoint: - cryostat-scorecard-tests - operator-install - image: "quay.io/thvo/cryostat-operator-scorecard:2.5.0-20231211070342" + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824" labels: suite: cryostat test: operator-install @@ -18,7 +18,7 @@ entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: "quay.io/thvo/cryostat-operator-scorecard:2.5.0-20231211070342" + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824" labels: suite: cryostat test: cryostat-cr From 59994adb3e08e5686bf624122c366d29f3598afb Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Sun, 10 Dec 2023 23:31:50 -0800 Subject: [PATCH 07/39] chore(bundle): correct image tag in bundle --- bundle/manifests/cryostat-operator.clusterserviceversion.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index 32039efc..5828de40 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -54,7 +54,7 @@ metadata: capabilities: Seamless Upgrades categories: Monitoring, Developer Tools containerImage: quay.io/cryostat/cryostat-operator:2.5.0-dev - createdAt: "2024-02-12T19:48:04Z" + createdAt: "2024-02-12T19:48:19Z" description: JVM monitoring and profiling tool operatorframework.io/initialization-resource: |- { From 5b38b8c09cc9cf4e6b3a9a790b82b096c4fbabbf Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Sun, 10 Dec 2023 23:49:30 -0800 Subject: [PATCH 08/39] fix(bundle): add missing scorecard test config patch --- .../cryostat-operator.clusterserviceversion.yaml | 2 +- bundle/tests/scorecard/config.yaml | 10 ++++++++++ config/scorecard/patches/custom.config.yaml | 10 ++++++++++ .../custom-scorecard-tests/rbac/scorecard_role.yaml | 6 ------ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index 5828de40..7534d9c8 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -54,7 +54,7 @@ metadata: capabilities: Seamless Upgrades categories: Monitoring, Developer Tools containerImage: quay.io/cryostat/cryostat-operator:2.5.0-dev - createdAt: "2024-02-12T19:48:19Z" + createdAt: "2024-02-12T19:48:34Z" description: JVM monitoring and profiling tool operatorframework.io/initialization-resource: |- { diff --git a/bundle/tests/scorecard/config.yaml b/bundle/tests/scorecard/config.yaml index 9a353826..2705ef04 100644 --- a/bundle/tests/scorecard/config.yaml +++ b/bundle/tests/scorecard/config.yaml @@ -86,6 +86,16 @@ stages: storage: spec: mountPath: {} + - entrypoint: + - cryostat-scorecard-tests + - cryostat-recording + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824 + labels: + suite: cryostat + test: cryostat-recording + storage: + spec: + mountPath: {} storage: spec: mountPath: {} diff --git a/config/scorecard/patches/custom.config.yaml b/config/scorecard/patches/custom.config.yaml index 011d7364..b2a99ec2 100644 --- a/config/scorecard/patches/custom.config.yaml +++ b/config/scorecard/patches/custom.config.yaml @@ -22,3 +22,13 @@ labels: suite: cryostat test: cryostat-cr +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - cryostat-scorecard-tests + - cryostat-recording + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824" + labels: + suite: cryostat + test: cryostat-recording diff --git a/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml b/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml index 867d6f58..ccef5617 100644 --- a/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml +++ b/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml @@ -94,13 +94,7 @@ rules: - namespaces verbs: - create ---- # Permissions for default OAuth configurations -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: oauth-client -rules: - apiGroups: - operator.cryostat.io resources: From f53e2682887860ab1a9d4ca14cb54be41cad31e3 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 11 Dec 2023 18:24:21 -0800 Subject: [PATCH 09/39] feat(scorecard): scaffold cryostat API client --- internal/test/scorecard/clients.go | 164 +++++++++++++++++++++++------ 1 file changed, 131 insertions(+), 33 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index e9f0a907..5a2c7dbd 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -15,7 +15,11 @@ package scorecard import ( + "bytes" "context" + "encoding/json" + "fmt" + "io" "net/http" "net/url" @@ -27,7 +31,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" ) // CryostatClientset is a Kubernetes Clientset that can also @@ -45,7 +48,7 @@ func (c *CryostatClientset) OperatorCRDs() *OperatorCRDClient { // NewClientset creates a CryostatClientset func NewClientset() (*CryostatClientset, error) { // Get in-cluster REST config from pod - config, err := ctrl.GetConfig() + config, err := rest.InClusterConfig() if err != nil { return nil, err } @@ -169,8 +172,11 @@ func delete(ctx context.Context, c rest.Interface, res string, ns string, name s // the Cryostat API type CryostatRESTClientset struct { // Application URL pointing to Cryostat - base *url.URL - v1Client *APIV1Client + TargetClient *TargetClient +} + +func (cs *CryostatRESTClientset) Targets() *TargetClient { + return cs.TargetClient } func NewCryostatRESTClientset(applicationURL string) (*CryostatRESTClientset, error) { @@ -179,54 +185,146 @@ func NewCryostatRESTClientset(applicationURL string) (*CryostatRESTClientset, er return nil, err } return &CryostatRESTClientset{ - base: base, - v1Client: &APIV1Client{}, + TargetClient: &TargetClient{ + Base: base, + }, }, nil } -func (cs *CryostatRESTClientset) APIV1(applicationURL string) *APIV1Client { - return cs.v1Client +// Client for Cryostat Target resources +type TargetClient struct { + Base *url.URL } -type TargetInterface interface { - List() []runtime.Object +func (client *TargetClient) List() ([]Target, error) { + return nil, nil } -type RecordingInterface interface { - Get(target string, name string) []runtime.Object - Delete(target string, name string) error +// Client for Cryostat Recording resources +type RecordingClient struct { + Base *url.URL } -type CryostatAPIClient interface { - Version() string - Prefix() string +func (client *RecordingClient) Get(ctx context.Context, recordingName string) ([]Target, error) { + return nil, nil } -type APIV1Client struct{} +func (client *RecordingClient) Create(ctx context.Context, options *RecordingCreateOptions) (*Recording, error) { + req, err := NewCryostatRESTReqruest(http.MethodPost, options) + if err != nil { + return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } -func (v1 *APIV1Client) Version() string { - return "v1" + r := &Recording{} + err = req.Do(r) + if err != nil { + return nil, fmt.Errorf("failed to create recording: %s", err.Error()) + } + return r, nil +} + +type CryostatAPIResources interface { + Target | Recording } -func (v1 *APIV1Client) Prefix() string { - return "/api/v1" +type Target struct { + ConnectUrl string `json:"connectUrl"` + Alias string `json:"alias,omitempty"` } -// func (v1 *APIV1Client) Targets() TargetInterface { -// return &TargetInterface{ -// List() { +type RecordingCreateOptions struct { + RecordingName string `json:"recordingName"` + Events string `json:"events"` + Duration int32 `json:"duration,omitempty"` + ToDisk bool `json:"toDisk,omitempty"` + MaxSize int32 `json:"maxSize,omitempty"` + MaxAge int32 `json:"maxAge,omitempty"` +} -// } -// } -// } +type Recording struct { + DownloadURL string `json:"downloadUrl"` + ReportURL string `json:"reportUrl"` + Id string `json:"id"` + Name string `json:"name"` + StartTime int32 `json:"startTime"` + Duration int32 `json:"duration"` + Continuous bool `json:"continuous"` + ToDisk bool `json:"toDisk"` + MaxSize int32 `json:"maxSize"` + MaxAge int32 `json:"maxAge"` +} // CryostatRESTRequest type CryostatRESTRequest struct { - base *url.URL - verb string - headers http.Header - params url.Values + URL *url.URL + Verb string + Headers http.Header + Params url.Values + Body io.Reader + OpenShift bool +} + +func (r *CryostatRESTRequest) Do(result any) error { + // Construct a complete URL with params + query := r.URL.Query() + for key, values := range r.Params { + for _, value := range values { + query.Add(key, value) + } + } + r.URL.RawQuery = query.Encode() + + // Add Auth Header + err := r.SetAuthHeader() + if err != nil { + return err + } + + request, err := http.NewRequest(r.Verb, r.URL.RequestURI(), r.Body) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(request) + if err != nil { + return err + } + defer resp.Body.Close() + + bodyAsBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %s", err.Error()) + } + + err = json.Unmarshal(bodyAsBytes, result) + if err != nil { + return fmt.Errorf("failed to JSON decode response body: %s", err.Error()) + } + + return nil +} - APIversion string - path string +func (r *CryostatRESTRequest) SetAuthHeader() error { + header := r.Headers + // Authentication is only enabled on OCP (currently) + if r.OpenShift { + config, err := rest.InClusterConfig() + if err != nil { + return fmt.Errorf("failed to get in cluster config: %s", err.Error()) + } + header.Add("Authorization", fmt.Sprintf("Bearer %s", config.BearerToken)) + } + return nil +} + +func NewCryostatRESTReqruest(verb string, body any) (*CryostatRESTRequest, error) { + _body, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to JSON encode recording option: %s", err.Error()) + } + req := &CryostatRESTRequest{ + Verb: http.MethodPost, + Body: bytes.NewReader(_body), + } + return req, nil } From 09061f7f7c5defc9461ff893c3e8ea8025aaa999 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 11 Dec 2023 19:31:45 -0800 Subject: [PATCH 10/39] chore(scorecard): clean up API client --- internal/test/scorecard/clients.go | 73 ++++++++++++++---------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 5a2c7dbd..6330b5d6 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -209,8 +209,8 @@ func (client *RecordingClient) Get(ctx context.Context, recordingName string) ([ return nil, nil } -func (client *RecordingClient) Create(ctx context.Context, options *RecordingCreateOptions) (*Recording, error) { - req, err := NewCryostatRESTReqruest(http.MethodPost, options) +func (client *RecordingClient) Create(ctx context.Context, target Target, options *RecordingCreateOptions) (*Recording, error) { + req, err := NewCryostatRESTRequest(client.Base.JoinPath(client.APIPath(target)), http.MethodPost, options) if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } @@ -223,6 +223,10 @@ func (client *RecordingClient) Create(ctx context.Context, options *RecordingCre return r, nil } +func (client *RecordingClient) APIPath(target Target) string { + return url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings", target.ConnectUrl)) +} + type CryostatAPIResources interface { Target | Recording } @@ -256,36 +260,20 @@ type Recording struct { // CryostatRESTRequest type CryostatRESTRequest struct { - URL *url.URL - Verb string - Headers http.Header - Params url.Values - Body io.Reader - OpenShift bool + URL *url.URL + Verb string + Header http.Header + Body io.Reader } func (r *CryostatRESTRequest) Do(result any) error { - // Construct a complete URL with params - query := r.URL.Query() - for key, values := range r.Params { - for _, value := range values { - query.Add(key, value) - } - } - r.URL.RawQuery = query.Encode() - - // Add Auth Header - err := r.SetAuthHeader() - if err != nil { - return err - } - - request, err := http.NewRequest(r.Verb, r.URL.RequestURI(), r.Body) + req, err := http.NewRequest(r.Verb, r.URL.String(), r.Body) if err != nil { return err } + addHeaders(req, r.Header) - resp, err := http.DefaultClient.Do(request) + resp, err := http.DefaultClient.Do(req) if err != nil { return err } @@ -304,27 +292,34 @@ func (r *CryostatRESTRequest) Do(result any) error { return nil } -func (r *CryostatRESTRequest) SetAuthHeader() error { - header := r.Headers - // Authentication is only enabled on OCP (currently) - if r.OpenShift { - config, err := rest.InClusterConfig() - if err != nil { - return fmt.Errorf("failed to get in cluster config: %s", err.Error()) +func addHeaders(req *http.Request, header http.Header) { + // Construct a complete URL with params + for key, values := range header { + for _, value := range values { + req.Header.Add(key, value) } - header.Add("Authorization", fmt.Sprintf("Bearer %s", config.BearerToken)) } - return nil } -func NewCryostatRESTReqruest(verb string, body any) (*CryostatRESTRequest, error) { +func NewCryostatRESTRequest(url *url.URL, verb string, body any) (*CryostatRESTRequest, error) { + req := &CryostatRESTRequest{ + URL: url, + Verb: http.MethodPost, + Header: http.Header{}, + } + _body, err := json.Marshal(body) if err != nil { - return nil, fmt.Errorf("failed to JSON encode recording option: %s", err.Error()) + return nil, fmt.Errorf("failed to JSON encode request body: %s", err.Error()) } - req := &CryostatRESTRequest{ - Verb: http.MethodPost, - Body: bytes.NewReader(_body), + req.Body = bytes.NewReader(_body) + + // Authentication is only enabled on OCP. Ignored on k8s. + config, err := rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("failed to get in-cluster configurations: %s", err.Error()) } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", config.BearerToken)) + return req, nil } From 29268e2c87786e2d8b6b992c029e7bb88c44908b Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Tue, 12 Dec 2023 06:28:11 -0800 Subject: [PATCH 11/39] test(scorecard): implement recording scorecard test --- internal/test/scorecard/clients.go | 117 ++++++++++++++++++++++++++--- internal/test/scorecard/tests.go | 67 ++++++++++++++++- 2 files changed, 173 insertions(+), 11 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 6330b5d6..6481a013 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -172,11 +172,16 @@ func delete(ctx context.Context, c rest.Interface, res string, ns string, name s // the Cryostat API type CryostatRESTClientset struct { // Application URL pointing to Cryostat - TargetClient *TargetClient + *TargetClient + *RecordingClient } -func (cs *CryostatRESTClientset) Targets() *TargetClient { - return cs.TargetClient +func (c *CryostatRESTClientset) Targets() *TargetClient { + return c.TargetClient +} + +func (c *CryostatRESTClientset) Recordings() *RecordingClient { + return c.RecordingClient } func NewCryostatRESTClientset(applicationURL string) (*CryostatRESTClientset, error) { @@ -205,26 +210,83 @@ type RecordingClient struct { Base *url.URL } -func (client *RecordingClient) Get(ctx context.Context, recordingName string) ([]Target, error) { +func (client *RecordingClient) Get(target Target, recordingName string) (*Recording, error) { + apiPath := url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", target.ConnectUrl, recordingName)) + req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodGet, nil) + if err != nil { + return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } + + recs := []Recording{} + err = req.SendForJSON(&recs) + if err != nil { + return nil, fmt.Errorf("failed to get recording: %s", err.Error()) + } + + for _, rec := range recs { + if rec.Name == recordingName { + return &rec, nil + } + } + return nil, nil } -func (client *RecordingClient) Create(ctx context.Context, target Target, options *RecordingCreateOptions) (*Recording, error) { - req, err := NewCryostatRESTRequest(client.Base.JoinPath(client.APIPath(target)), http.MethodPost, options) +func (client *RecordingClient) Create(target Target, options *RecordingCreateOptions) (*Recording, error) { + apiPath := url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings", target.ConnectUrl)) + req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodPost, options) if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } r := &Recording{} - err = req.Do(r) + err = req.SendForJSON(r) if err != nil { return nil, fmt.Errorf("failed to create recording: %s", err.Error()) } return r, nil } -func (client *RecordingClient) APIPath(target Target) string { - return url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings", target.ConnectUrl)) +func (client *RecordingClient) Archive(target Target, recordingName string) (*string, error) { + apiPath := url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", target.ConnectUrl, recordingName)) + req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodPatch, "SAVE") + if err != nil { + return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } + + archiveName, err := req.SendForPlainText() + if err != nil { + return nil, fmt.Errorf("failed to archive recording: %s", err.Error()) + } + return archiveName, nil +} + +func (client *RecordingClient) Stop(target Target, recordingName string) error { + apiPath := url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", target.ConnectUrl, recordingName)) + req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodPatch, "STOP") + if err != nil { + return fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } + + err = req.SendForJSON(nil) + if err != nil { + return fmt.Errorf("failed to stop recording: %s", err.Error()) + } + return nil +} + +func (client *RecordingClient) Delete(target Target, recordingName string) error { + apiPath := url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", target.ConnectUrl, recordingName)) + req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodDelete, nil) + if err != nil { + return fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } + + err = req.SendForJSON(nil) + if err != nil { + return fmt.Errorf("failed to delete recording: %s", err.Error()) + } + return nil } type CryostatAPIResources interface { @@ -251,6 +313,7 @@ type Recording struct { Id string `json:"id"` Name string `json:"name"` StartTime int32 `json:"startTime"` + State string `json:"state"` Duration int32 `json:"duration"` Continuous bool `json:"continuous"` ToDisk bool `json:"toDisk"` @@ -266,7 +329,33 @@ type CryostatRESTRequest struct { Body io.Reader } -func (r *CryostatRESTRequest) Do(result any) error { +func (r *CryostatRESTRequest) SendForPlainText() (*string, error) { + req, err := http.NewRequest(r.Verb, r.URL.String(), r.Body) + if err != nil { + return nil, err + } + addHeaders(req, r.Header) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if !statusOK(resp.StatusCode) { + return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) + } + + bodyAsBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %s", err.Error()) + } + + body := string(bodyAsBytes) + return &body, nil +} + +func (r *CryostatRESTRequest) SendForJSON(result any) error { req, err := http.NewRequest(r.Verb, r.URL.String(), r.Body) if err != nil { return err @@ -279,6 +368,10 @@ func (r *CryostatRESTRequest) Do(result any) error { } defer resp.Body.Close() + if !statusOK(resp.StatusCode) { + return fmt.Errorf("API request failed with status code: %d", resp.StatusCode) + } + bodyAsBytes, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response body: %s", err.Error()) @@ -301,6 +394,10 @@ func addHeaders(req *http.Request, header http.Header) { } } +func statusOK(statusCode int) bool { + return statusCode >= 200 && statusCode < 300 +} + func NewCryostatRESTRequest(url *url.URL, verb string, body any) (*CryostatRESTRequest, error) { req := &CryostatRESTRequest{ URL: url, diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 0d535f0d..f0988899 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -85,7 +85,72 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh err = createAndWaitForCryostat(cr, tr) if err != nil { - return fail(*r, fmt.Sprintf("%s test failed: %s", CryostatRecordingTestName, err.Error())) + return fail(*r, fmt.Sprintf("failed to determine application URL: %s", err.Error())) } + + apiClient, err := NewCryostatRESTClientset(cr.Status.ApplicationURL) + if err != nil { + return fail(*r, fmt.Sprintf("failed to get Cryostat API client: %s", err.Error())) + } + + // Get a target + targets, err := apiClient.Targets().List() + if err != nil { + return fail(*r, fmt.Sprintf("failed to list discovered targets: %s", err.Error())) + } + if len(targets) == 0 { + return fail(*r, "cryostat failed to discover any targets") + } + target := targets[0] + r.Log += fmt.Sprintf("using target: %+v", target) + + // Create a recording + options := &RecordingCreateOptions{ + RecordingName: "scorecard-test-rec", + Events: "template=ALL", + Duration: 0, + ToDisk: true, + MaxSize: 0, + MaxAge: 0, + } + recording, err := apiClient.Recordings().Create(target, options) + if err != nil { + return fail(*r, fmt.Sprintf("failed to create a recording: %s", err.Error())) + } + r.Log += fmt.Sprintf("created a recording: %+v", recording) + + // Archive the recording + archiveName, err := apiClient.Recordings().Archive(target, recording.Name) + if err != nil { + return fail(*r, fmt.Sprintf("failed to archive the recording: %s", err.Error())) + } + r.Log += fmt.Sprintf("archived the recording %s at: %s", recording.Name, *archiveName) + + // Stop the recording + err = apiClient.Recordings().Stop(target, recording.Name) + if err != nil { + return fail(*r, fmt.Sprintf("failed to stop the recording %s: %s", recording.Name, err.Error())) + } + + recording, err = apiClient.Recordings().Get(target, recording.Name) + if err != nil { + return fail(*r, fmt.Sprintf("failed to get the recordings: %s", err.Error())) + } + if recording == nil { + return fail(*r, fmt.Sprintf("recording %s does not exist: %s", recording.Name, err.Error())) + } + + if recording.State != "STOPPED" { + return fail(*r, fmt.Sprintf("recording %s failed to stop: %s", recording.Name, err.Error())) + } + r.Log += fmt.Sprintf("stopped the recording: %s", recording.Name) + + // Delete the recording + err = apiClient.Recordings().Delete(target, recording.Name) + if err != nil { + return fail(*r, fmt.Sprintf("failed to delete the recording %s: %s", recording.Name, err.Error())) + } + r.Log += fmt.Sprintf("deleted the recording: %s", recording.Name) + return *r } From f794fbd891bccd537b8a7b658d3adfe14ab540f1 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Tue, 12 Dec 2023 15:42:27 -0800 Subject: [PATCH 12/39] fixup(scorecard): correctly add scorecard test via hack templates --- .../cryostat-operator.clusterserviceversion.yaml | 2 +- bundle/tests/scorecard/config.yaml | 6 +++--- config/scorecard/patches/custom.config.yaml | 6 +++--- hack/custom.config.yaml.in | 10 ++++++++++ 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index 7534d9c8..4cbf41ce 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -54,7 +54,7 @@ metadata: capabilities: Seamless Upgrades categories: Monitoring, Developer Tools containerImage: quay.io/cryostat/cryostat-operator:2.5.0-dev - createdAt: "2024-02-12T19:48:34Z" + createdAt: "2024-02-12T19:49:05Z" description: JVM monitoring and profiling tool operatorframework.io/initialization-resource: |- { diff --git a/bundle/tests/scorecard/config.yaml b/bundle/tests/scorecard/config.yaml index 2705ef04..a25c7191 100644 --- a/bundle/tests/scorecard/config.yaml +++ b/bundle/tests/scorecard/config.yaml @@ -69,7 +69,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - operator-install - image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824 + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113 labels: suite: cryostat test: operator-install @@ -79,7 +79,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824 + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113 labels: suite: cryostat test: cryostat-cr @@ -89,7 +89,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-recording - image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824 + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113 labels: suite: cryostat test: cryostat-recording diff --git a/config/scorecard/patches/custom.config.yaml b/config/scorecard/patches/custom.config.yaml index b2a99ec2..05c0f06b 100644 --- a/config/scorecard/patches/custom.config.yaml +++ b/config/scorecard/patches/custom.config.yaml @@ -8,7 +8,7 @@ entrypoint: - cryostat-scorecard-tests - operator-install - image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824" + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113" labels: suite: cryostat test: operator-install @@ -18,7 +18,7 @@ entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824" + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113" labels: suite: cryostat test: cryostat-cr @@ -28,7 +28,7 @@ entrypoint: - cryostat-scorecard-tests - cryostat-recording - image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231211072824" + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113" labels: suite: cryostat test: cryostat-recording diff --git a/hack/custom.config.yaml.in b/hack/custom.config.yaml.in index d6bc3e71..c73f06c0 100644 --- a/hack/custom.config.yaml.in +++ b/hack/custom.config.yaml.in @@ -21,3 +21,13 @@ labels: suite: cryostat test: cryostat-cr +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - cryostat-scorecard-tests + - cryostat-recording + image: "${CUSTOM_SCORECARD_IMG}" + labels: + suite: cryostat + test: cryostat-recording From 65cdb96b7248c5c52ca9a0db7f8ebfb78920062c Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Tue, 12 Dec 2023 23:55:58 -0800 Subject: [PATCH 13/39] fix(client): ignore unverified tls certs and base64 oauth token --- internal/test/scorecard/clients.go | 75 ++++++++++++++++--------- internal/test/scorecard/common_utils.go | 16 +++--- internal/test/scorecard/tests.go | 45 +++++++++------ 3 files changed, 86 insertions(+), 50 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 6481a013..f773cb53 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -17,6 +17,8 @@ package scorecard import ( "bytes" "context" + "crypto/tls" + "encoding/base64" "encoding/json" "fmt" "io" @@ -171,7 +173,6 @@ func delete(ctx context.Context, c rest.Interface, res string, ns string, name s // CryostatRESTClientset contains methods to interact with // the Cryostat API type CryostatRESTClientset struct { - // Application URL pointing to Cryostat *TargetClient *RecordingClient } @@ -193,6 +194,9 @@ func NewCryostatRESTClientset(applicationURL string) (*CryostatRESTClientset, er TargetClient: &TargetClient{ Base: base, }, + RecordingClient: &RecordingClient{ + Base: base, + }, }, nil } @@ -202,7 +206,19 @@ type TargetClient struct { } func (client *TargetClient) List() ([]Target, error) { - return nil, nil + apiPath := "/api/v1/targets" + req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodGet, nil) + if err != nil { + return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } + + targets := []Target{} + err = req.SendForJSON(&targets) + if err != nil { + return nil, err + } + + return targets, nil } // Client for Cryostat Recording resources @@ -211,7 +227,7 @@ type RecordingClient struct { } func (client *RecordingClient) Get(target Target, recordingName string) (*Recording, error) { - apiPath := url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", target.ConnectUrl, recordingName)) + apiPath := fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(target.ConnectUrl), recordingName) req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodGet, nil) if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) @@ -220,7 +236,7 @@ func (client *RecordingClient) Get(target Target, recordingName string) (*Record recs := []Recording{} err = req.SendForJSON(&recs) if err != nil { - return nil, fmt.Errorf("failed to get recording: %s", err.Error()) + return nil, err } for _, rec := range recs { @@ -233,7 +249,7 @@ func (client *RecordingClient) Get(target Target, recordingName string) (*Record } func (client *RecordingClient) Create(target Target, options *RecordingCreateOptions) (*Recording, error) { - apiPath := url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings", target.ConnectUrl)) + apiPath := fmt.Sprintf("/api/v1/targets/%s/recordings", url.PathEscape(target.ConnectUrl)) req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodPost, options) if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) @@ -242,13 +258,13 @@ func (client *RecordingClient) Create(target Target, options *RecordingCreateOpt r := &Recording{} err = req.SendForJSON(r) if err != nil { - return nil, fmt.Errorf("failed to create recording: %s", err.Error()) + return nil, err } return r, nil } func (client *RecordingClient) Archive(target Target, recordingName string) (*string, error) { - apiPath := url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", target.ConnectUrl, recordingName)) + apiPath := fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(target.ConnectUrl), recordingName) req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodPatch, "SAVE") if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) @@ -256,13 +272,13 @@ func (client *RecordingClient) Archive(target Target, recordingName string) (*st archiveName, err := req.SendForPlainText() if err != nil { - return nil, fmt.Errorf("failed to archive recording: %s", err.Error()) + return nil, err } return archiveName, nil } func (client *RecordingClient) Stop(target Target, recordingName string) error { - apiPath := url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", target.ConnectUrl, recordingName)) + apiPath := fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(target.ConnectUrl), recordingName) req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodPatch, "STOP") if err != nil { return fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) @@ -270,13 +286,13 @@ func (client *RecordingClient) Stop(target Target, recordingName string) error { err = req.SendForJSON(nil) if err != nil { - return fmt.Errorf("failed to stop recording: %s", err.Error()) + return err } return nil } func (client *RecordingClient) Delete(target Target, recordingName string) error { - apiPath := url.PathEscape(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", target.ConnectUrl, recordingName)) + apiPath := fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(target.ConnectUrl), recordingName) req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodDelete, nil) if err != nil { return fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) @@ -284,7 +300,7 @@ func (client *RecordingClient) Delete(target Target, recordingName string) error err = req.SendForJSON(nil) if err != nil { - return fmt.Errorf("failed to delete recording: %s", err.Error()) + return err } return nil } @@ -336,22 +352,21 @@ func (r *CryostatRESTRequest) SendForPlainText() (*string, error) { } addHeaders(req, r.Header) - resp, err := http.DefaultClient.Do(req) + resp, err := NewHttpClient().Do(req) if err != nil { return nil, err } defer resp.Body.Close() - if !statusOK(resp.StatusCode) { - return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) - } - bodyAsBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %s", err.Error()) } - body := string(bodyAsBytes) + if !statusOK(resp.StatusCode) { + return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, body) + } + return &body, nil } @@ -362,16 +377,12 @@ func (r *CryostatRESTRequest) SendForJSON(result any) error { } addHeaders(req, r.Header) - resp, err := http.DefaultClient.Do(req) + resp, err := NewHttpClient().Do(req) if err != nil { return err } defer resp.Body.Close() - if !statusOK(resp.StatusCode) { - return fmt.Errorf("API request failed with status code: %d", resp.StatusCode) - } - bodyAsBytes, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response body: %s", err.Error()) @@ -382,6 +393,10 @@ func (r *CryostatRESTRequest) SendForJSON(result any) error { return fmt.Errorf("failed to JSON decode response body: %s", err.Error()) } + if !statusOK(resp.StatusCode) { + return fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, result) + } + return nil } @@ -401,7 +416,7 @@ func statusOK(statusCode int) bool { func NewCryostatRESTRequest(url *url.URL, verb string, body any) (*CryostatRESTRequest, error) { req := &CryostatRESTRequest{ URL: url, - Verb: http.MethodPost, + Verb: verb, Header: http.Header{}, } @@ -416,7 +431,17 @@ func NewCryostatRESTRequest(url *url.URL, verb string, body any) (*CryostatRESTR if err != nil { return nil, fmt.Errorf("failed to get in-cluster configurations: %s", err.Error()) } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", config.BearerToken)) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte(config.BearerToken)))) return req, nil } + +func NewHttpClient() *http.Client { + client := *http.DefaultClient + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + return &client +} diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go index 356ff921..7f654f49 100644 --- a/internal/test/scorecard/common_utils.go +++ b/internal/test/scorecard/common_utils.go @@ -296,14 +296,14 @@ func newCryostatCR(namespace string, withIngress bool) *operatorv1beta1.Cryostat return cr } -func createAndWaitForCryostat(cr *operatorv1beta1.Cryostat, resources *TestResources) error { +func createAndWaitForCryostat(cr *operatorv1beta1.Cryostat, resources *TestResources) (*operatorv1beta1.Cryostat, error) { client := resources.Client r := resources.TestResult cr, err := client.OperatorCRDs().Cryostats(cr.Namespace).Create(context.Background(), cr) if err != nil { logError(r, fmt.Sprintf("failed to create Cryostat CR: %s", err.Error())) - return err + return nil, err } // Poll the deployment until it becomes available or we timeout @@ -312,7 +312,7 @@ func createAndWaitForCryostat(cr *operatorv1beta1.Cryostat, resources *TestResou err = waitForDeploymentAvailability(ctx, client, cr.Namespace, cr.Name, r) if err != nil { logError(r, fmt.Sprintf("Cryostat main deployment did not become available: %s", err.Error())) - return err + return nil, err } err = wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { @@ -323,16 +323,16 @@ func createAndWaitForCryostat(cr *operatorv1beta1.Cryostat, resources *TestResou if len(cr.Status.ApplicationURL) > 0 { return true, nil } - r.Log += "Application URL is not yet available\n" + r.Log += "application URL is not yet available\n" return false, nil }) if err != nil { - logError(r, fmt.Sprintf("Application URL not found in CR: %s", err.Error())) - return err + logError(r, fmt.Sprintf("application URL not found in CR: %s", err.Error())) + return nil, err } - r.Log += fmt.Sprintf("Application is ready at %s\n", cr.Status.ApplicationURL) + r.Log += fmt.Sprintf("application is ready at %s\n", cr.Status.ApplicationURL) - return nil + return cr, nil } func cleanupCryostat(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace string) { diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index f0988899..f3e69173 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -17,9 +17,11 @@ package scorecard import ( "context" "fmt" + "time" scapiv1alpha3 "github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3" apimanifests "github.com/operator-framework/api/pkg/manifests" + "k8s.io/apimachinery/pkg/util/wait" ) const ( @@ -60,18 +62,17 @@ func CryostatCRTest(bundle *apimanifests.Bundle, namespace string, openShiftCert } // Create a default Cryostat CR - cr := newCryostatCR(namespace, !tr.OpenShift) - defer cleanupCryostat(r, tr.Client, namespace) - - err = createAndWaitForCryostat(cr, tr) + _, err = createAndWaitForCryostat(newCryostatCR(namespace, !tr.OpenShift), tr) if err != nil { return fail(*r, fmt.Sprintf("%s test failed: %s", CryostatCRTestName, err.Error())) } + defer cleanupCryostat(r, tr.Client, namespace) + return *r } func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openShiftCertManager bool) scapiv1alpha3.TestResult { - tr := newTestResources(CryostatCRTestName) + tr := newTestResources(CryostatRecordingTestName) r := tr.TestResult err := setupCRTestResources(tr, openShiftCertManager) @@ -80,29 +81,39 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh } // Create a default Cryostat CR - cr := newCryostatCR(namespace, !tr.OpenShift) - defer cleanupCryostat(r, tr.Client, namespace) - - err = createAndWaitForCryostat(cr, tr) + cr, err := createAndWaitForCryostat(newCryostatCR(namespace, !tr.OpenShift), tr) if err != nil { return fail(*r, fmt.Sprintf("failed to determine application URL: %s", err.Error())) } + defer cleanupCryostat(r, tr.Client, namespace) apiClient, err := NewCryostatRESTClientset(cr.Status.ApplicationURL) if err != nil { return fail(*r, fmt.Sprintf("failed to get Cryostat API client: %s", err.Error())) } - // Get a target - targets, err := apiClient.Targets().List() + // Get a target for test. + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + var target Target + err = wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { + targets, err := apiClient.Targets().List() + if err != nil { + logError(r, fmt.Sprintf("failed to list discovered targets: %s", err.Error())) + return false, err + } + if len(targets) == 0 { + r.Log += "no target is yet discovered\n" + return false, nil // Try again + } + target = targets[0] + r.Log += fmt.Sprintf("found target: %+v", target) + return true, nil + }) if err != nil { - return fail(*r, fmt.Sprintf("failed to list discovered targets: %s", err.Error())) - } - if len(targets) == 0 { - return fail(*r, "cryostat failed to discover any targets") + return fail(*r, fmt.Sprintf("failed to get a target for test: %s", err.Error())) } - target := targets[0] - r.Log += fmt.Sprintf("using target: %+v", target) // Create a recording options := &RecordingCreateOptions{ From 4cce99e4788f5d867f9777f57a222501a4685edd Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Wed, 13 Dec 2023 22:17:21 -0800 Subject: [PATCH 14/39] chore(bundle): split cryostat tests to separate stage --- .../cryostat-operator.clusterserviceversion.yaml | 2 +- bundle/tests/scorecard/config.yaml | 7 ++++--- config/scorecard/bases/config.yaml | 4 +++- config/scorecard/patches/custom.config.yaml | 12 ++++++------ hack/custom.config.yaml.in | 6 +++--- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index 4cbf41ce..b3209ff4 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -54,7 +54,7 @@ metadata: capabilities: Seamless Upgrades categories: Monitoring, Developer Tools containerImage: quay.io/cryostat/cryostat-operator:2.5.0-dev - createdAt: "2024-02-12T19:49:05Z" + createdAt: "2024-02-12T19:49:13Z" description: JVM monitoring and profiling tool operatorframework.io/initialization-resource: |- { diff --git a/bundle/tests/scorecard/config.yaml b/bundle/tests/scorecard/config.yaml index a25c7191..cf894137 100644 --- a/bundle/tests/scorecard/config.yaml +++ b/bundle/tests/scorecard/config.yaml @@ -66,10 +66,11 @@ stages: storage: spec: mountPath: {} +- tests: - entrypoint: - cryostat-scorecard-tests - operator-install - image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113 + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502 labels: suite: cryostat test: operator-install @@ -79,7 +80,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113 + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502 labels: suite: cryostat test: cryostat-cr @@ -89,7 +90,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-recording - image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113 + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502 labels: suite: cryostat test: cryostat-recording diff --git a/config/scorecard/bases/config.yaml b/config/scorecard/bases/config.yaml index c7704784..7924d1df 100644 --- a/config/scorecard/bases/config.yaml +++ b/config/scorecard/bases/config.yaml @@ -3,5 +3,7 @@ kind: Configuration metadata: name: config stages: -- parallel: true +- parallel: true # Build-in Tests + tests: [] +- parallel: false # Cryostat Custom Tests tests: [] diff --git a/config/scorecard/patches/custom.config.yaml b/config/scorecard/patches/custom.config.yaml index 05c0f06b..cda5bfd2 100644 --- a/config/scorecard/patches/custom.config.yaml +++ b/config/scorecard/patches/custom.config.yaml @@ -3,32 +3,32 @@ path: /serviceaccount value: cryostat-scorecard - op: add - path: /stages/0/tests/- + path: /stages/1/tests/- value: entrypoint: - cryostat-scorecard-tests - operator-install - image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113" + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502" labels: suite: cryostat test: operator-install - op: add - path: /stages/0/tests/- + path: /stages/1/tests/- value: entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113" + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502" labels: suite: cryostat test: cryostat-cr - op: add - path: /stages/0/tests/- + path: /stages/1/tests/- value: entrypoint: - cryostat-scorecard-tests - cryostat-recording - image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231212234113" + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502" labels: suite: cryostat test: cryostat-recording diff --git a/hack/custom.config.yaml.in b/hack/custom.config.yaml.in index c73f06c0..48746200 100644 --- a/hack/custom.config.yaml.in +++ b/hack/custom.config.yaml.in @@ -2,7 +2,7 @@ path: /serviceaccount value: cryostat-scorecard - op: add - path: /stages/0/tests/- + path: /stages/1/tests/- value: entrypoint: - cryostat-scorecard-tests @@ -12,7 +12,7 @@ suite: cryostat test: operator-install - op: add - path: /stages/0/tests/- + path: /stages/1/tests/- value: entrypoint: - cryostat-scorecard-tests @@ -22,7 +22,7 @@ suite: cryostat test: cryostat-cr - op: add - path: /stages/0/tests/- + path: /stages/1/tests/- value: entrypoint: - cryostat-scorecard-tests From 843fc0794905b915310c811ed4e16441f76e6a32 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Wed, 13 Dec 2023 23:16:44 -0800 Subject: [PATCH 15/39] fix(scorecard): extend default transport instead of overwriting --- internal/test/scorecard/clients.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index f773cb53..4a06abfd 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -437,11 +437,12 @@ func NewCryostatRESTRequest(url *url.URL, verb string, body any) (*CryostatRESTR } func NewHttpClient() *http.Client { - client := *http.DefaultClient - client.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - } - return &client + client := &http.Client{} + // Ignore verifying certs + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + client.Transport = transport + return client } From 9d42c0b87f2f69d52fa3521e0075d86aaa10507c Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Thu, 14 Dec 2023 04:56:38 -0800 Subject: [PATCH 16/39] chore(scorecard): refactor client to support multi-part --- internal/test/scorecard/clients.go | 256 +++++++++++++---------------- internal/test/scorecard/tests.go | 17 +- internal/test/scorecard/types.go | 74 +++++++++ 3 files changed, 193 insertions(+), 154 deletions(-) create mode 100644 internal/test/scorecard/types.go diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 4a06abfd..00437cb8 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -15,7 +15,6 @@ package scorecard import ( - "bytes" "context" "crypto/tls" "encoding/base64" @@ -24,6 +23,7 @@ import ( "io" "net/http" "net/url" + "strings" operatorv1beta1 "github.com/cryostatio/cryostat-operator/api/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -190,12 +190,15 @@ func NewCryostatRESTClientset(applicationURL string) (*CryostatRESTClientset, er if err != nil { return nil, err } + httpClient := NewHttpClient() return &CryostatRESTClientset{ TargetClient: &TargetClient{ - Base: base, + Base: base, + Client: httpClient, }, RecordingClient: &RecordingClient{ - Base: base, + Base: base, + Client: httpClient, }, }, nil } @@ -203,246 +206,207 @@ func NewCryostatRESTClientset(applicationURL string) (*CryostatRESTClientset, er // Client for Cryostat Target resources type TargetClient struct { Base *url.URL + *http.Client } -func (client *TargetClient) List() ([]Target, error) { - apiPath := "/api/v1/targets" - req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodGet, nil) +func (client *TargetClient) List(ctx context.Context) ([]Target, error) { + url := client.Base.JoinPath("/api/v1/targets") + req, err := NewHttpRequest(ctx, url.String(), http.MethodGet, nil) if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } - targets := []Target{} - err = req.SendForJSON(&targets) + resp, err := client.Do(req) if err != nil { return nil, err } + if !StatusOK(resp.StatusCode) { + return nil, fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) + } + + targets := make([]Target, 0) + err = ReadJSON(resp, &targets) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %s", err.Error()) + } + return targets, nil } // Client for Cryostat Recording resources type RecordingClient struct { Base *url.URL + *http.Client } -func (client *RecordingClient) Get(target Target, recordingName string) (*Recording, error) { - apiPath := fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(target.ConnectUrl), recordingName) - req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodGet, nil) +func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recordingName string) (*Recording, error) { + url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), recordingName)) + req, err := NewHttpRequest(ctx, http.MethodGet, url.String(), nil) if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } - recs := []Recording{} - err = req.SendForJSON(&recs) + resp, err := client.Do(req) if err != nil { return nil, err } - for _, rec := range recs { + if !StatusOK(resp.StatusCode) { + return nil, fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) + } + + recordings := make([]Recording, 0) + err = ReadJSON(resp, &recordings) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %s", err.Error()) + } + + for _, rec := range recordings { if rec.Name == recordingName { return &rec, nil } } - return nil, nil } -func (client *RecordingClient) Create(target Target, options *RecordingCreateOptions) (*Recording, error) { - apiPath := fmt.Sprintf("/api/v1/targets/%s/recordings", url.PathEscape(target.ConnectUrl)) - req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodPost, options) +func (client *RecordingClient) Create(ctx context.Context, connectUrl string, options *RecordingCreateOptions) (*Recording, error) { + url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings", url.PathEscape(connectUrl))) + body, err := options.ToMultiPart() + if err != nil { + return nil, fmt.Errorf("failed to create a form multi-part: %s", err.Error()) + } + req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } + req.Header.Add("Content-type", "multipart/form-data") - r := &Recording{} - err = req.SendForJSON(r) + resp, err := client.Do(req) if err != nil { return nil, err } - return r, nil -} -func (client *RecordingClient) Archive(target Target, recordingName string) (*string, error) { - apiPath := fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(target.ConnectUrl), recordingName) - req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodPatch, "SAVE") - if err != nil { - return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + if !StatusOK(resp.StatusCode) { + return nil, fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) } - archiveName, err := req.SendForPlainText() + recording := &Recording{} + err = ReadJSON(resp, recording) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read response body: %s", err.Error()) } - return archiveName, nil + return recording, err } -func (client *RecordingClient) Stop(target Target, recordingName string) error { - apiPath := fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(target.ConnectUrl), recordingName) - req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodPatch, "STOP") +func (client *RecordingClient) Archive(ctx context.Context, connectUrl string, recordingName string) (string, error) { + url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), recordingName)) + body := strings.NewReader("SAVE") + req, err := NewHttpRequest(ctx, http.MethodDelete, url.String(), body) if err != nil { - return fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + return "", fmt.Errorf("failed to create a REST request: %s", err.Error()) } + req.Header.Add("Content-type", "text/plain") - err = req.SendForJSON(nil) + resp, err := client.Do(req) if err != nil { - return err + return "", err } - return nil -} -func (client *RecordingClient) Delete(target Target, recordingName string) error { - apiPath := fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(target.ConnectUrl), recordingName) - req, err := NewCryostatRESTRequest(client.Base.JoinPath(apiPath), http.MethodDelete, nil) - if err != nil { - return fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + if !StatusOK(resp.StatusCode) { + return "", fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) } - err = req.SendForJSON(nil) + bodyAsString, err := ReadString(resp) if err != nil { - return err + return "", fmt.Errorf("failed to read response body: %s", err.Error()) } - return nil -} - -type CryostatAPIResources interface { - Target | Recording -} - -type Target struct { - ConnectUrl string `json:"connectUrl"` - Alias string `json:"alias,omitempty"` + return bodyAsString, nil } -type RecordingCreateOptions struct { - RecordingName string `json:"recordingName"` - Events string `json:"events"` - Duration int32 `json:"duration,omitempty"` - ToDisk bool `json:"toDisk,omitempty"` - MaxSize int32 `json:"maxSize,omitempty"` - MaxAge int32 `json:"maxAge,omitempty"` -} - -type Recording struct { - DownloadURL string `json:"downloadUrl"` - ReportURL string `json:"reportUrl"` - Id string `json:"id"` - Name string `json:"name"` - StartTime int32 `json:"startTime"` - State string `json:"state"` - Duration int32 `json:"duration"` - Continuous bool `json:"continuous"` - ToDisk bool `json:"toDisk"` - MaxSize int32 `json:"maxSize"` - MaxAge int32 `json:"maxAge"` -} - -// CryostatRESTRequest -type CryostatRESTRequest struct { - URL *url.URL - Verb string - Header http.Header - Body io.Reader -} - -func (r *CryostatRESTRequest) SendForPlainText() (*string, error) { - req, err := http.NewRequest(r.Verb, r.URL.String(), r.Body) +func (client *RecordingClient) Stop(ctx context.Context, connectUrl string, recordingName string) error { + url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), recordingName)) + body := strings.NewReader("STOP") + req, err := NewHttpRequest(ctx, http.MethodDelete, url.String(), body) if err != nil { - return nil, err + return fmt.Errorf("failed to create a REST request: %s", err.Error()) } - addHeaders(req, r.Header) + req.Header.Add("Content-type", "text/plain") - resp, err := NewHttpClient().Do(req) + resp, err := client.Do(req) if err != nil { - return nil, err + return err } - defer resp.Body.Close() - bodyAsBytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %s", err.Error()) - } - body := string(bodyAsBytes) - if !statusOK(resp.StatusCode) { - return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, body) + if !StatusOK(resp.StatusCode) { + return fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) } - - return &body, nil + return nil } -func (r *CryostatRESTRequest) SendForJSON(result any) error { - req, err := http.NewRequest(r.Verb, r.URL.String(), r.Body) +func (client *RecordingClient) Delete(ctx context.Context, connectUrl string, recordingName string) error { + url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), recordingName)) + req, err := NewHttpRequest(ctx, http.MethodDelete, url.String(), nil) if err != nil { - return err + return fmt.Errorf("failed to create a REST request: %s", err.Error()) } - addHeaders(req, r.Header) - resp, err := NewHttpClient().Do(req) + resp, err := client.Do(req) if err != nil { return err } - defer resp.Body.Close() - - bodyAsBytes, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %s", err.Error()) + if !StatusOK(resp.StatusCode) { + return fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) } + return nil +} - err = json.Unmarshal(bodyAsBytes, result) +func ReadJSON(resp *http.Response, result any) error { + body, err := io.ReadAll(resp.Body) if err != nil { - return fmt.Errorf("failed to JSON decode response body: %s", err.Error()) + return nil } - if !statusOK(resp.StatusCode) { - return fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, result) + err = json.Unmarshal(body, result) + if err != nil { + return nil } - return nil } -func addHeaders(req *http.Request, header http.Header) { - // Construct a complete URL with params - for key, values := range header { - for _, value := range values { - req.Header.Add(key, value) - } +func ReadString(resp *http.Response) (string, error) { + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err } + return string(body), nil } -func statusOK(statusCode int) bool { - return statusCode >= 200 && statusCode < 300 -} - -func NewCryostatRESTRequest(url *url.URL, verb string, body any) (*CryostatRESTRequest, error) { - req := &CryostatRESTRequest{ - URL: url, - Verb: verb, - Header: http.Header{}, +func NewHttpClient() *http.Client { + client := &http.Client{} + // Ignore verifying certs + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, } + client.Transport = transport + return client +} - _body, err := json.Marshal(body) +func NewHttpRequest(ctx context.Context, method string, url string, body io.Reader) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, method, url, body) if err != nil { - return nil, fmt.Errorf("failed to JSON encode request body: %s", err.Error()) + return nil, err } - req.Body = bytes.NewReader(_body) - // Authentication is only enabled on OCP. Ignored on k8s. config, err := rest.InClusterConfig() if err != nil { return nil, fmt.Errorf("failed to get in-cluster configurations: %s", err.Error()) } req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte(config.BearerToken)))) - return req, nil } -func NewHttpClient() *http.Client { - client := &http.Client{} - // Ignore verifying certs - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, - } - client.Transport = transport - return client +func StatusOK(statusCode int) bool { + return statusCode >= 200 && statusCode < 300 } diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index f3e69173..0502b610 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -98,7 +98,7 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh var target Target err = wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { - targets, err := apiClient.Targets().List() + targets, err := apiClient.Targets().List(context.Background()) if err != nil { logError(r, fmt.Sprintf("failed to list discovered targets: %s", err.Error())) return false, err @@ -108,12 +108,13 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh return false, nil // Try again } target = targets[0] - r.Log += fmt.Sprintf("found target: %+v", target) + r.Log += fmt.Sprintf("found a target: %+v", target) return true, nil }) if err != nil { return fail(*r, fmt.Sprintf("failed to get a target for test: %s", err.Error())) } + connectUrl := target.ConnectUrl // Create a recording options := &RecordingCreateOptions{ @@ -124,26 +125,26 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh MaxSize: 0, MaxAge: 0, } - recording, err := apiClient.Recordings().Create(target, options) + recording, err := apiClient.Recordings().Create(context.Background(), connectUrl, options) if err != nil { return fail(*r, fmt.Sprintf("failed to create a recording: %s", err.Error())) } r.Log += fmt.Sprintf("created a recording: %+v", recording) // Archive the recording - archiveName, err := apiClient.Recordings().Archive(target, recording.Name) + archiveName, err := apiClient.Recordings().Archive(context.Background(), connectUrl, recording.Name) if err != nil { return fail(*r, fmt.Sprintf("failed to archive the recording: %s", err.Error())) } - r.Log += fmt.Sprintf("archived the recording %s at: %s", recording.Name, *archiveName) + r.Log += fmt.Sprintf("archived the recording %s at: %s", recording.Name, archiveName) // Stop the recording - err = apiClient.Recordings().Stop(target, recording.Name) + err = apiClient.Recordings().Stop(context.Background(), connectUrl, recording.Name) if err != nil { return fail(*r, fmt.Sprintf("failed to stop the recording %s: %s", recording.Name, err.Error())) } - recording, err = apiClient.Recordings().Get(target, recording.Name) + recording, err = apiClient.Recordings().Get(context.Background(), connectUrl, recording.Name) if err != nil { return fail(*r, fmt.Sprintf("failed to get the recordings: %s", err.Error())) } @@ -157,7 +158,7 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh r.Log += fmt.Sprintf("stopped the recording: %s", recording.Name) // Delete the recording - err = apiClient.Recordings().Delete(target, recording.Name) + err = apiClient.Recordings().Delete(context.Background(), connectUrl, recording.Name) if err != nil { return fail(*r, fmt.Sprintf("failed to delete the recording %s: %s", recording.Name, err.Error())) } diff --git a/internal/test/scorecard/types.go b/internal/test/scorecard/types.go new file mode 100644 index 00000000..f81520e7 --- /dev/null +++ b/internal/test/scorecard/types.go @@ -0,0 +1,74 @@ +// Copyright The Cryostat Authors. +// +// 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 scorecard + +import ( + "bytes" + "errors" + "io" + "mime/multipart" + "strconv" +) + +type RecordingCreateOptions struct { + RecordingName string `json:"recordingName"` + Events string `json:"events"` + Duration int32 `json:"duration,omitempty"` + ToDisk bool `json:"toDisk,omitempty"` + MaxSize int32 `json:"maxSize,omitempty"` + MaxAge int32 `json:"maxAge,omitempty"` +} + +func (opts *RecordingCreateOptions) ToMultiPart() (io.Reader, error) { + form := &bytes.Buffer{} + writer := multipart.NewWriter(form) + + errs := make([]error, 0) + + errs = append(errs, + writer.WriteField("recordingName", opts.RecordingName), + writer.WriteField("events", opts.Events), + writer.WriteField("duration", strconv.Itoa(int(opts.Duration))), + writer.WriteField("toDisk", strconv.FormatBool(opts.ToDisk)), + writer.WriteField("maxSize", strconv.Itoa(int(opts.MaxSize))), + writer.WriteField("maxAge", strconv.Itoa(int(opts.MaxAge))), + ) + + writer.Close() + + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + return bytes.NewReader(form.Bytes()), nil +} + +type Recording struct { + DownloadURL string `json:"downloadUrl"` + ReportURL string `json:"reportUrl"` + Id string `json:"id"` + Name string `json:"name"` + StartTime int32 `json:"startTime"` + State string `json:"state"` + Duration int32 `json:"duration"` + Continuous bool `json:"continuous"` + ToDisk bool `json:"toDisk"` + MaxSize int32 `json:"maxSize"` + MaxAge int32 `json:"maxAge"` +} + +type Target struct { + ConnectUrl string `json:"connectUrl"` + Alias string `json:"alias,omitempty"` +} From 29f38d2b4113047dfb5ac60e67f234c92b59e207 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Thu, 14 Dec 2023 05:13:25 -0800 Subject: [PATCH 17/39] fixup(client): fix request verb --- internal/test/scorecard/clients.go | 24 +++++++++-------- internal/test/scorecard/types.go | 41 +++++++++++++----------------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 00437cb8..af805a65 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -211,7 +211,7 @@ type TargetClient struct { func (client *TargetClient) List(ctx context.Context) ([]Target, error) { url := client.Base.JoinPath("/api/v1/targets") - req, err := NewHttpRequest(ctx, url.String(), http.MethodGet, nil) + req, err := NewHttpRequest(ctx, http.MethodGet, url.String(), nil) if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } @@ -222,7 +222,7 @@ func (client *TargetClient) List(ctx context.Context) ([]Target, error) { } if !StatusOK(resp.StatusCode) { - return nil, fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) + return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } targets := make([]Target, 0) @@ -253,7 +253,7 @@ func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recor } if !StatusOK(resp.StatusCode) { - return nil, fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) + return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } recordings := make([]Recording, 0) @@ -272,10 +272,7 @@ func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recor func (client *RecordingClient) Create(ctx context.Context, connectUrl string, options *RecordingCreateOptions) (*Recording, error) { url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings", url.PathEscape(connectUrl))) - body, err := options.ToMultiPart() - if err != nil { - return nil, fmt.Errorf("failed to create a form multi-part: %s", err.Error()) - } + body := options.ToMultiPart() req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) @@ -288,7 +285,7 @@ func (client *RecordingClient) Create(ctx context.Context, connectUrl string, op } if !StatusOK(resp.StatusCode) { - return nil, fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) + return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } recording := &Recording{} @@ -314,7 +311,7 @@ func (client *RecordingClient) Archive(ctx context.Context, connectUrl string, r } if !StatusOK(resp.StatusCode) { - return "", fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) + return "", fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } bodyAsString, err := ReadString(resp) @@ -339,7 +336,7 @@ func (client *RecordingClient) Stop(ctx context.Context, connectUrl string, reco } if !StatusOK(resp.StatusCode) { - return fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) + return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } return nil } @@ -356,7 +353,7 @@ func (client *RecordingClient) Delete(ctx context.Context, connectUrl string, re return err } if !StatusOK(resp.StatusCode) { - return fmt.Errorf("API request failed with status code %d: %+v", resp.StatusCode, resp.Status) + return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } return nil } @@ -382,6 +379,11 @@ func ReadString(resp *http.Response) (string, error) { return string(body), nil } +func ReadError(resp *http.Response) string { + body, _ := ReadString(resp) + return string(body) +} + func NewHttpClient() *http.Client { client := &http.Client{} // Ignore verifying certs diff --git a/internal/test/scorecard/types.go b/internal/test/scorecard/types.go index f81520e7..4c29b8e5 100644 --- a/internal/test/scorecard/types.go +++ b/internal/test/scorecard/types.go @@ -16,42 +16,35 @@ package scorecard import ( "bytes" - "errors" "io" "mime/multipart" + "net/url" "strconv" ) type RecordingCreateOptions struct { - RecordingName string `json:"recordingName"` - Events string `json:"events"` - Duration int32 `json:"duration,omitempty"` - ToDisk bool `json:"toDisk,omitempty"` - MaxSize int32 `json:"maxSize,omitempty"` - MaxAge int32 `json:"maxAge,omitempty"` + RecordingName string + Events string + Duration int32 + ToDisk bool + MaxSize int32 + MaxAge int32 } -func (opts *RecordingCreateOptions) ToMultiPart() (io.Reader, error) { - form := &bytes.Buffer{} - writer := multipart.NewWriter(form) +func (opts *RecordingCreateOptions) ToMultiPart() io.Reader { + formBuffer := &bytes.Buffer{} + writer := multipart.NewWriter(formBuffer) - errs := make([]error, 0) - - errs = append(errs, - writer.WriteField("recordingName", opts.RecordingName), - writer.WriteField("events", opts.Events), - writer.WriteField("duration", strconv.Itoa(int(opts.Duration))), - writer.WriteField("toDisk", strconv.FormatBool(opts.ToDisk)), - writer.WriteField("maxSize", strconv.Itoa(int(opts.MaxSize))), - writer.WriteField("maxAge", strconv.Itoa(int(opts.MaxAge))), - ) + writer.WriteField("recordingName", url.PathEscape(opts.RecordingName)) + writer.WriteField("events", opts.Events) + writer.WriteField("duration", strconv.Itoa(int(opts.Duration))) + writer.WriteField("toDisk", strconv.FormatBool(opts.ToDisk)) + writer.WriteField("maxSize", strconv.Itoa(int(opts.MaxSize))) + writer.WriteField("maxAge", strconv.Itoa(int(opts.MaxAge))) writer.Close() - if len(errs) > 0 { - return nil, errors.Join(errs...) - } - return bytes.NewReader(form.Bytes()), nil + return bytes.NewReader(formBuffer.Bytes()) } type Recording struct { From 62c2899ff02a3114d1774a69abca3be922b3fc95 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Thu, 14 Dec 2023 06:10:52 -0800 Subject: [PATCH 18/39] fix(client): fix recording create form format --- internal/test/scorecard/clients.go | 4 ++-- internal/test/scorecard/types.go | 24 +++++++++--------------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index af805a65..3d7d5596 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -272,12 +272,12 @@ func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recor func (client *RecordingClient) Create(ctx context.Context, connectUrl string, options *RecordingCreateOptions) (*Recording, error) { url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings", url.PathEscape(connectUrl))) - body := options.ToMultiPart() + body := strings.NewReader(options.ToFormData()) req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } - req.Header.Add("Content-type", "multipart/form-data") + req.Header.Add("Content-type", "application/x-www-form-urlencoded") resp, err := client.Do(req) if err != nil { diff --git a/internal/test/scorecard/types.go b/internal/test/scorecard/types.go index 4c29b8e5..6d3ff63f 100644 --- a/internal/test/scorecard/types.go +++ b/internal/test/scorecard/types.go @@ -15,9 +15,6 @@ package scorecard import ( - "bytes" - "io" - "mime/multipart" "net/url" "strconv" ) @@ -31,20 +28,17 @@ type RecordingCreateOptions struct { MaxAge int32 } -func (opts *RecordingCreateOptions) ToMultiPart() io.Reader { - formBuffer := &bytes.Buffer{} - writer := multipart.NewWriter(formBuffer) +func (opts *RecordingCreateOptions) ToFormData() string { + formData := &url.Values{} - writer.WriteField("recordingName", url.PathEscape(opts.RecordingName)) - writer.WriteField("events", opts.Events) - writer.WriteField("duration", strconv.Itoa(int(opts.Duration))) - writer.WriteField("toDisk", strconv.FormatBool(opts.ToDisk)) - writer.WriteField("maxSize", strconv.Itoa(int(opts.MaxSize))) - writer.WriteField("maxAge", strconv.Itoa(int(opts.MaxAge))) + formData.Add("recordingName", url.PathEscape(opts.RecordingName)) + formData.Add("events", opts.Events) + formData.Add("duration", strconv.Itoa(int(opts.Duration))) + formData.Add("toDisk", strconv.FormatBool(opts.ToDisk)) + formData.Add("maxSize", strconv.Itoa(int(opts.MaxSize))) + formData.Add("maxAge", strconv.Itoa(int(opts.MaxAge))) - writer.Close() - - return bytes.NewReader(formBuffer.Bytes()) + return formData.Encode() } type Recording struct { From 7a128e79d30cc1c9f47802dfce07263a02dc9561 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Thu, 14 Dec 2023 17:37:56 -0800 Subject: [PATCH 19/39] fix(scorecard): create stored credentials for target JVM --- .../rbac/scorecard_role.yaml | 6 +++ internal/test/scorecard/clients.go | 41 +++++++++++++++++-- internal/test/scorecard/tests.go | 21 +++++++++- internal/test/scorecard/types.go | 16 ++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml b/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml index ccef5617..a59a079f 100644 --- a/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml +++ b/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml @@ -53,6 +53,12 @@ rules: - cryostats/status verbs: - get +- apiGroups: + - "" + resources: + - secrets + verbs: + - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 3d7d5596..d7ad55c4 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -173,8 +173,9 @@ func delete(ctx context.Context, c rest.Interface, res string, ns string, name s // CryostatRESTClientset contains methods to interact with // the Cryostat API type CryostatRESTClientset struct { - *TargetClient - *RecordingClient + TargetClient *TargetClient + RecordingClient *RecordingClient + CredentialClient *CredentialClient } func (c *CryostatRESTClientset) Targets() *TargetClient { @@ -185,6 +186,10 @@ func (c *CryostatRESTClientset) Recordings() *RecordingClient { return c.RecordingClient } +func (c *CryostatRESTClientset) Credential() *CredentialClient { + return c.CredentialClient +} + func NewCryostatRESTClientset(applicationURL string) (*CryostatRESTClientset, error) { base, err := url.Parse(applicationURL) if err != nil { @@ -200,6 +205,10 @@ func NewCryostatRESTClientset(applicationURL string) (*CryostatRESTClientset, er Base: base, Client: httpClient, }, + CredentialClient: &CredentialClient{ + Base: base, + Client: httpClient, + }, }, nil } @@ -358,7 +367,7 @@ func (client *RecordingClient) Delete(ctx context.Context, connectUrl string, re return nil } -func ReadJSON(resp *http.Response, result any) error { +func ReadJSON(resp *http.Response, result interface{}) error { body, err := io.ReadAll(resp.Body) if err != nil { return nil @@ -412,3 +421,29 @@ func NewHttpRequest(ctx context.Context, method string, url string, body io.Read func StatusOK(statusCode int) bool { return statusCode >= 200 && statusCode < 300 } + +type CredentialClient struct { + Base *url.URL + *http.Client +} + +func (client *CredentialClient) Create(ctx context.Context, credential *Credential) error { + url := client.Base.JoinPath("/api/v2.2/credentials") + body := strings.NewReader(credential.ToFormData()) + req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) + if err != nil { + return fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } + req.Header.Add("Content-type", "application/x-www-form-urlencoded") + + resp, err := client.Do(req) + if err != nil { + return err + } + + if !StatusOK(resp.StatusCode) { + return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + } + + return nil +} diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 0502b610..07adbad8 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -21,6 +21,7 @@ import ( scapiv1alpha3 "github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3" apimanifests "github.com/operator-framework/api/pkg/manifests" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" ) @@ -107,7 +108,7 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh r.Log += "no target is yet discovered\n" return false, nil // Try again } - target = targets[0] + target = targets[0] // Cryostat r.Log += fmt.Sprintf("found a target: %+v", target) return true, nil }) @@ -116,6 +117,24 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh } connectUrl := target.ConnectUrl + jmxSecretName := cryostastCRName + "-jmx-auth" + secret, err := tr.Client.CoreV1().Secrets(namespace).Get(context.Background(), jmxSecretName, metav1.GetOptions{}) + if err != nil { + return fail(*r, fmt.Sprintf("failed to get jmx credentials: %s", err.Error())) + } + + credential := &Credential{ + UserName: string(secret.Data["CRYOSTAT_RJMX_USER"]), + Password: string(secret.Data["CRYOSTAT_RJMX_PASS"]), + MatchExpression: fmt.Sprintf("target.alias==\"%s\"", target.Alias), + } + + err = apiClient.CredentialClient.Create(context.Background(), credential) + if err != nil { + return fail(*r, fmt.Sprintf("failed to create stored credential: %s", err.Error())) + } + r.Log += fmt.Sprintf("created stored credential with match expression: %s", credential.MatchExpression) + // Create a recording options := &RecordingCreateOptions{ RecordingName: "scorecard-test-rec", diff --git a/internal/test/scorecard/types.go b/internal/test/scorecard/types.go index 6d3ff63f..853530c2 100644 --- a/internal/test/scorecard/types.go +++ b/internal/test/scorecard/types.go @@ -41,6 +41,22 @@ func (opts *RecordingCreateOptions) ToFormData() string { return formData.Encode() } +type Credential struct { + UserName string + Password string + MatchExpression string +} + +func (cred *Credential) ToFormData() string { + formData := &url.Values{} + + formData.Add("username", cred.UserName) + formData.Add("password", cred.Password) + formData.Add("matchExpression", cred.MatchExpression) + + return formData.Encode() +} + type Recording struct { DownloadURL string `json:"downloadUrl"` ReportURL string `json:"reportUrl"` From 3fb6a3531bb1ceff5517d25ebe80edfffa89c2fb Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Thu, 14 Dec 2023 19:08:59 -0800 Subject: [PATCH 20/39] fix(scorecard): fix 502 status error --- internal/test/scorecard/clients.go | 116 ++++++++++++++---------- internal/test/scorecard/common_utils.go | 52 +++++++++-- internal/test/scorecard/tests.go | 50 ++++++---- internal/test/scorecard/types.go | 2 +- 4 files changed, 143 insertions(+), 77 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index d7ad55c4..129a631d 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -16,7 +16,6 @@ package scorecard import ( "context" - "crypto/tls" "encoding/base64" "encoding/json" "fmt" @@ -190,11 +189,7 @@ func (c *CryostatRESTClientset) Credential() *CredentialClient { return c.CredentialClient } -func NewCryostatRESTClientset(applicationURL string) (*CryostatRESTClientset, error) { - base, err := url.Parse(applicationURL) - if err != nil { - return nil, err - } +func NewCryostatRESTClientset(base *url.URL) *CryostatRESTClientset { httpClient := NewHttpClient() return &CryostatRESTClientset{ TargetClient: &TargetClient{ @@ -209,7 +204,7 @@ func NewCryostatRESTClientset(applicationURL string) (*CryostatRESTClientset, er Base: base, Client: httpClient, }, - }, nil + } } // Client for Cryostat Target resources @@ -224,6 +219,7 @@ func (client *TargetClient) List(ctx context.Context) ([]Target, error) { if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } + req.Header.Add("Accept", "*/*") resp, err := client.Do(req) if err != nil { @@ -240,6 +236,8 @@ func (client *TargetClient) List(ctx context.Context) ([]Target, error) { return nil, fmt.Errorf("failed to read response body: %s", err.Error()) } + defer resp.Body.Close() + return targets, nil } @@ -250,11 +248,12 @@ type RecordingClient struct { } func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recordingName string) (*Recording, error) { - url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), recordingName)) + url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings", url.PathEscape(connectUrl))) req, err := NewHttpRequest(ctx, http.MethodGet, url.String(), nil) if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } + req.Header.Add("Accept", "*/*") resp, err := client.Do(req) if err != nil { @@ -276,7 +275,10 @@ func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recor return &rec, nil } } - return nil, nil + + defer resp.Body.Close() + + return nil, fmt.Errorf("recording %s does not exist for target %s", recordingName, connectUrl) } func (client *RecordingClient) Create(ctx context.Context, connectUrl string, options *RecordingCreateOptions) (*Recording, error) { @@ -286,7 +288,8 @@ func (client *RecordingClient) Create(ctx context.Context, connectUrl string, op if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } - req.Header.Add("Content-type", "application/x-www-form-urlencoded") + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Accept", "*/*") resp, err := client.Do(req) if err != nil { @@ -302,17 +305,21 @@ func (client *RecordingClient) Create(ctx context.Context, connectUrl string, op if err != nil { return nil, fmt.Errorf("failed to read response body: %s", err.Error()) } + + defer resp.Body.Close() + return recording, err } func (client *RecordingClient) Archive(ctx context.Context, connectUrl string, recordingName string) (string, error) { - url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), recordingName)) + url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), url.PathEscape(recordingName))) body := strings.NewReader("SAVE") - req, err := NewHttpRequest(ctx, http.MethodDelete, url.String(), body) + req, err := NewHttpRequest(ctx, http.MethodPatch, url.String(), body) if err != nil { return "", fmt.Errorf("failed to create a REST request: %s", err.Error()) } - req.Header.Add("Content-type", "text/plain") + req.Header.Add("Content-Type", "text/plain") + req.Header.Add("Accept", "*/*") resp, err := client.Do(req) if err != nil { @@ -327,17 +334,20 @@ func (client *RecordingClient) Archive(ctx context.Context, connectUrl string, r if err != nil { return "", fmt.Errorf("failed to read response body: %s", err.Error()) } + + defer resp.Body.Close() + return bodyAsString, nil } func (client *RecordingClient) Stop(ctx context.Context, connectUrl string, recordingName string) error { - url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), recordingName)) + url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), url.PathEscape(recordingName))) body := strings.NewReader("STOP") - req, err := NewHttpRequest(ctx, http.MethodDelete, url.String(), body) + req, err := NewHttpRequest(ctx, http.MethodPatch, url.String(), body) if err != nil { return fmt.Errorf("failed to create a REST request: %s", err.Error()) } - req.Header.Add("Content-type", "text/plain") + req.Header.Add("Content-Type", "text/plain") resp, err := client.Do(req) if err != nil { @@ -347,11 +357,14 @@ func (client *RecordingClient) Stop(ctx context.Context, connectUrl string, reco if !StatusOK(resp.StatusCode) { return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } + + defer resp.Body.Close() + return nil } func (client *RecordingClient) Delete(ctx context.Context, connectUrl string, recordingName string) error { - url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), recordingName)) + url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), url.PathEscape(recordingName))) req, err := NewHttpRequest(ctx, http.MethodDelete, url.String(), nil) if err != nil { return fmt.Errorf("failed to create a REST request: %s", err.Error()) @@ -364,6 +377,37 @@ func (client *RecordingClient) Delete(ctx context.Context, connectUrl string, re if !StatusOK(resp.StatusCode) { return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } + + defer resp.Body.Close() + + return nil +} + +type CredentialClient struct { + Base *url.URL + *http.Client +} + +func (client *CredentialClient) Create(ctx context.Context, credential *Credential) error { + url := client.Base.JoinPath("/api/v2.2/credentials") + body := strings.NewReader(credential.ToFormData()) + req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) + if err != nil { + return fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + resp, err := client.Do(req) + if err != nil { + return err + } + + if !StatusOK(resp.StatusCode) { + return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + } + + defer resp.Body.Close() + return nil } @@ -394,12 +438,14 @@ func ReadError(resp *http.Response) string { } func NewHttpClient() *http.Client { - client := &http.Client{} - // Ignore verifying certs - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, + client := &http.Client{ + Timeout: testTimeout, } + + transport := http.DefaultTransport.(*http.Transport).Clone() + // Ignore verifying certs + transport.TLSClientConfig.InsecureSkipVerify = true + client.Transport = transport return client } @@ -421,29 +467,3 @@ func NewHttpRequest(ctx context.Context, method string, url string, body io.Read func StatusOK(statusCode int) bool { return statusCode >= 200 && statusCode < 300 } - -type CredentialClient struct { - Base *url.URL - *http.Client -} - -func (client *CredentialClient) Create(ctx context.Context, credential *Credential) error { - url := client.Base.JoinPath("/api/v2.2/credentials") - body := strings.NewReader(credential.ToFormData()) - req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) - if err != nil { - return fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) - } - req.Header.Add("Content-type", "application/x-www-form-urlencoded") - - resp, err := client.Do(req) - if err != nil { - return err - } - - if !StatusOK(resp.StatusCode) { - return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) - } - - return nil -} diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go index 7f654f49..5c0d279a 100644 --- a/internal/test/scorecard/common_utils.go +++ b/internal/test/scorecard/common_utils.go @@ -17,6 +17,8 @@ package scorecard import ( "context" "fmt" + "net/http" + "net/url" "time" operatorv1beta1 "github.com/cryostatio/cryostat-operator/api/v1beta1" @@ -34,7 +36,6 @@ import ( const ( operatorDeploymentName string = "cryostat-operator-controller-manager" - cryostastCRName string = "cryostat-cr-test" testTimeout time.Duration = time.Minute * 10 ) @@ -214,10 +215,10 @@ func setupCRTestResources(tr *TestResources, openShiftCertManager bool) error { return nil } -func newCryostatCR(namespace string, withIngress bool) *operatorv1beta1.Cryostat { +func newCryostatCR(name string, namespace string, withIngress bool) *operatorv1beta1.Cryostat { cr := &operatorv1beta1.Cryostat{ ObjectMeta: metav1.ObjectMeta{ - Name: cryostastCRName, + Name: name, Namespace: namespace, }, Spec: operatorv1beta1.CryostatSpec{ @@ -246,7 +247,7 @@ func newCryostatCR(namespace string, withIngress bool) *operatorv1beta1.Cryostat PathType: &pathType, Backend: netv1.IngressBackend{ Service: &netv1.IngressServiceBackend{ - Name: cryostastCRName, + Name: name, Port: netv1.ServiceBackendPort{ Number: 8181, }, @@ -277,7 +278,7 @@ func newCryostatCR(namespace string, withIngress bool) *operatorv1beta1.Cryostat PathType: &pathType, Backend: netv1.IngressBackend{ Service: &netv1.IngressServiceBackend{ - Name: fmt.Sprintf("%s-grafana", cryostastCRName), + Name: fmt.Sprintf("%s-grafana", name), Port: netv1.ServiceBackendPort{ Number: 3000, }, @@ -296,7 +297,7 @@ func newCryostatCR(namespace string, withIngress bool) *operatorv1beta1.Cryostat return cr } -func createAndWaitForCryostat(cr *operatorv1beta1.Cryostat, resources *TestResources) (*operatorv1beta1.Cryostat, error) { +func createAndWaitTillCryostatAvailable(cr *operatorv1beta1.Cryostat, resources *TestResources) (*operatorv1beta1.Cryostat, error) { client := resources.Client r := resources.TestResult @@ -335,10 +336,45 @@ func createAndWaitForCryostat(cr *operatorv1beta1.Cryostat, resources *TestResou return cr, nil } -func cleanupCryostat(r *scapiv1alpha3.TestResult, client *CryostatClientset, namespace string) { +func waitTillCryostatReachable(base *url.URL, resources *TestResources) error { + client := NewHttpClient() + r := resources.TestResult + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + err := wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { + url := base.JoinPath("/health") + req, err := NewHttpRequest(ctx, http.MethodGet, url.String(), nil) + if err != nil { + return false, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } + req.Header.Add("Accept", "*/*") + + resp, err := client.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if !StatusOK(resp.StatusCode) { + if resp.StatusCode == http.StatusServiceUnavailable { + r.Log += fmt.Sprintf("application is not yet reachable at %s\n", url.String()) + return false, nil // Try again + } + return false, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + } + + return true, nil + }) + + return err +} + +func cleanupCryostat(r *scapiv1alpha3.TestResult, client *CryostatClientset, name string, namespace string) { cr := &operatorv1beta1.Cryostat{ ObjectMeta: metav1.ObjectMeta{ - Name: cryostastCRName, + Name: name, Namespace: namespace, }, } diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 07adbad8..163032fe 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -17,6 +17,7 @@ package scorecard import ( "context" "fmt" + "net/url" "time" scapiv1alpha3 "github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3" @@ -63,11 +64,11 @@ func CryostatCRTest(bundle *apimanifests.Bundle, namespace string, openShiftCert } // Create a default Cryostat CR - _, err = createAndWaitForCryostat(newCryostatCR(namespace, !tr.OpenShift), tr) + _, err = createAndWaitTillCryostatAvailable(newCryostatCR(CryostatCRTestName, namespace, !tr.OpenShift), tr) if err != nil { return fail(*r, fmt.Sprintf("%s test failed: %s", CryostatCRTestName, err.Error())) } - defer cleanupCryostat(r, tr.Client, namespace) + defer cleanupCryostat(r, tr.Client, CryostatCRTestName, namespace) return *r } @@ -82,22 +83,28 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh } // Create a default Cryostat CR - cr, err := createAndWaitForCryostat(newCryostatCR(namespace, !tr.OpenShift), tr) + cr, err := createAndWaitTillCryostatAvailable(newCryostatCR(CryostatRecordingTestName, namespace, !tr.OpenShift), tr) if err != nil { return fail(*r, fmt.Sprintf("failed to determine application URL: %s", err.Error())) } - defer cleanupCryostat(r, tr.Client, namespace) + defer cleanupCryostat(r, tr.Client, CryostatRecordingTestName, namespace) - apiClient, err := NewCryostatRESTClientset(cr.Status.ApplicationURL) + base, err := url.Parse(cr.Status.ApplicationURL) if err != nil { - return fail(*r, fmt.Sprintf("failed to get Cryostat API client: %s", err.Error())) + return fail(*r, fmt.Sprintf("application URL is invalid: %s", err.Error())) } - // Get a target for test. - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() + err = waitTillCryostatReachable(base, tr) + if err != nil { + return fail(*r, fmt.Sprintf("failed to reach the application: %s", err.Error())) + } + apiClient := NewCryostatRESTClientset(base) + + // Get a target for test var target Target + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() err = wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { targets, err := apiClient.Targets().List(context.Background()) if err != nil { @@ -109,7 +116,7 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh return false, nil // Try again } target = targets[0] // Cryostat - r.Log += fmt.Sprintf("found a target: %+v", target) + r.Log += fmt.Sprintf("found a target: %+v\n", target) return true, nil }) if err != nil { @@ -117,7 +124,7 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh } connectUrl := target.ConnectUrl - jmxSecretName := cryostastCRName + "-jmx-auth" + jmxSecretName := CryostatRecordingTestName + "-jmx-auth" secret, err := tr.Client.CoreV1().Secrets(namespace).Get(context.Background(), jmxSecretName, metav1.GetOptions{}) if err != nil { return fail(*r, fmt.Sprintf("failed to get jmx credentials: %s", err.Error())) @@ -133,13 +140,16 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh if err != nil { return fail(*r, fmt.Sprintf("failed to create stored credential: %s", err.Error())) } - r.Log += fmt.Sprintf("created stored credential with match expression: %s", credential.MatchExpression) + r.Log += fmt.Sprintf("created stored credential with match expression: %s\n", credential.MatchExpression) + + // Wait for Cryostat to update the discovery tree + time.Sleep(2 * time.Second) // Create a recording options := &RecordingCreateOptions{ RecordingName: "scorecard-test-rec", Events: "template=ALL", - Duration: 0, + Duration: 0, // Continuous ToDisk: true, MaxSize: 0, MaxAge: 0, @@ -148,14 +158,17 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh if err != nil { return fail(*r, fmt.Sprintf("failed to create a recording: %s", err.Error())) } - r.Log += fmt.Sprintf("created a recording: %+v", recording) + r.Log += fmt.Sprintf("created a recording: %+v\n", recording) + + // Allow the recording to run for 5s + time.Sleep(5 * time.Second) // Archive the recording archiveName, err := apiClient.Recordings().Archive(context.Background(), connectUrl, recording.Name) if err != nil { return fail(*r, fmt.Sprintf("failed to archive the recording: %s", err.Error())) } - r.Log += fmt.Sprintf("archived the recording %s at: %s", recording.Name, archiveName) + r.Log += fmt.Sprintf("archived the recording %s at: %s\n", recording.Name, archiveName) // Stop the recording err = apiClient.Recordings().Stop(context.Background(), connectUrl, recording.Name) @@ -167,21 +180,18 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh if err != nil { return fail(*r, fmt.Sprintf("failed to get the recordings: %s", err.Error())) } - if recording == nil { - return fail(*r, fmt.Sprintf("recording %s does not exist: %s", recording.Name, err.Error())) - } if recording.State != "STOPPED" { return fail(*r, fmt.Sprintf("recording %s failed to stop: %s", recording.Name, err.Error())) } - r.Log += fmt.Sprintf("stopped the recording: %s", recording.Name) + r.Log += fmt.Sprintf("stopped the recording: %s\n", recording.Name) // Delete the recording err = apiClient.Recordings().Delete(context.Background(), connectUrl, recording.Name) if err != nil { return fail(*r, fmt.Sprintf("failed to delete the recording %s: %s", recording.Name, err.Error())) } - r.Log += fmt.Sprintf("deleted the recording: %s", recording.Name) + r.Log += fmt.Sprintf("deleted the recording: %s\n", recording.Name) return *r } diff --git a/internal/test/scorecard/types.go b/internal/test/scorecard/types.go index 853530c2..6c7f73d4 100644 --- a/internal/test/scorecard/types.go +++ b/internal/test/scorecard/types.go @@ -31,7 +31,7 @@ type RecordingCreateOptions struct { func (opts *RecordingCreateOptions) ToFormData() string { formData := &url.Values{} - formData.Add("recordingName", url.PathEscape(opts.RecordingName)) + formData.Add("recordingName", opts.RecordingName) formData.Add("events", opts.Events) formData.Add("duration", strconv.Itoa(int(opts.Duration))) formData.Add("toDisk", strconv.FormatBool(opts.ToDisk)) From 1ba77811dee7bfc104972dd97a477e3c2c9c5e8a Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Sat, 16 Dec 2023 03:58:59 -0800 Subject: [PATCH 21/39] chore(scorecard): simplify client def --- internal/test/scorecard/clients.go | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 129a631d..351c760a 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -190,29 +190,34 @@ func (c *CryostatRESTClientset) Credential() *CredentialClient { } func NewCryostatRESTClientset(base *url.URL) *CryostatRESTClientset { - httpClient := NewHttpClient() + commonClient := &commonCryostatRESTClient{ + Base: base, + Client: NewHttpClient(), + } + return &CryostatRESTClientset{ TargetClient: &TargetClient{ - Base: base, - Client: httpClient, + commonCryostatRESTClient: commonClient, }, RecordingClient: &RecordingClient{ - Base: base, - Client: httpClient, + commonCryostatRESTClient: commonClient, }, CredentialClient: &CredentialClient{ - Base: base, - Client: httpClient, + commonCryostatRESTClient: commonClient, }, } } -// Client for Cryostat Target resources -type TargetClient struct { +type commonCryostatRESTClient struct { Base *url.URL *http.Client } +// Client for Cryostat Target resources +type TargetClient struct { + *commonCryostatRESTClient +} + func (client *TargetClient) List(ctx context.Context) ([]Target, error) { url := client.Base.JoinPath("/api/v1/targets") req, err := NewHttpRequest(ctx, http.MethodGet, url.String(), nil) @@ -243,8 +248,7 @@ func (client *TargetClient) List(ctx context.Context) ([]Target, error) { // Client for Cryostat Recording resources type RecordingClient struct { - Base *url.URL - *http.Client + *commonCryostatRESTClient } func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recordingName string) (*Recording, error) { @@ -384,8 +388,7 @@ func (client *RecordingClient) Delete(ctx context.Context, connectUrl string, re } type CredentialClient struct { - Base *url.URL - *http.Client + *commonCryostatRESTClient } func (client *CredentialClient) Create(ctx context.Context, credential *Credential) error { From 2ee2ec42d6a3c71edcf15fc75b0bc7d73c071bae Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Thu, 21 Dec 2023 02:18:43 -0800 Subject: [PATCH 22/39] chore(scorecard): fetch recordings to ensure action is correctly performed --- internal/test/scorecard/clients.go | 14 +++++++-- internal/test/scorecard/tests.go | 47 ++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 351c760a..c80ba98d 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -251,7 +251,7 @@ type RecordingClient struct { *commonCryostatRESTClient } -func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recordingName string) (*Recording, error) { +func (client *RecordingClient) List(ctx context.Context, connectUrl string) ([]Recording, error) { url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings", url.PathEscape(connectUrl))) req, err := NewHttpRequest(ctx, http.MethodGet, url.String(), nil) if err != nil { @@ -273,6 +273,16 @@ func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recor if err != nil { return nil, fmt.Errorf("failed to read response body: %s", err.Error()) } + defer resp.Body.Close() + + return recordings, nil +} + +func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recordingName string) (*Recording, error) { + recordings, err := client.List(ctx, connectUrl) + if err != nil { + return nil, err + } for _, rec := range recordings { if rec.Name == recordingName { @@ -280,8 +290,6 @@ func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recor } } - defer resp.Body.Close() - return nil, fmt.Errorf("recording %s does not exist for target %s", recordingName, connectUrl) } diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 163032fe..003ba195 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -154,44 +154,61 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh MaxSize: 0, MaxAge: 0, } - recording, err := apiClient.Recordings().Create(context.Background(), connectUrl, options) + rec, err := apiClient.Recordings().Create(context.Background(), connectUrl, options) if err != nil { return fail(*r, fmt.Sprintf("failed to create a recording: %s", err.Error())) } - r.Log += fmt.Sprintf("created a recording: %+v\n", recording) + r.Log += fmt.Sprintf("created a recording: %+v\n", rec) + + // View the current recording list after creating one + recs, err := apiClient.Recordings().List(context.Background(), connectUrl) + if err != nil { + return fail(*r, fmt.Sprintf("failed to list recordings: %s", err.Error())) + } + r.Log += fmt.Sprintf("current list of recordings: %+v\n", recs) // Allow the recording to run for 5s time.Sleep(5 * time.Second) // Archive the recording - archiveName, err := apiClient.Recordings().Archive(context.Background(), connectUrl, recording.Name) + archiveName, err := apiClient.Recordings().Archive(context.Background(), connectUrl, rec.Name) if err != nil { return fail(*r, fmt.Sprintf("failed to archive the recording: %s", err.Error())) } - r.Log += fmt.Sprintf("archived the recording %s at: %s\n", recording.Name, archiveName) + r.Log += fmt.Sprintf("archived the recording %s at: %s\n", rec.Name, archiveName) + + // TODO: Fetch archives and output to log + + // TODO: Generate a report // Stop the recording - err = apiClient.Recordings().Stop(context.Background(), connectUrl, recording.Name) + err = apiClient.Recordings().Stop(context.Background(), connectUrl, rec.Name) if err != nil { - return fail(*r, fmt.Sprintf("failed to stop the recording %s: %s", recording.Name, err.Error())) + return fail(*r, fmt.Sprintf("failed to stop the recording %s: %s", rec.Name, err.Error())) } - - recording, err = apiClient.Recordings().Get(context.Background(), connectUrl, recording.Name) + // Get the recording to verify its state + rec, err = apiClient.Recordings().Get(context.Background(), connectUrl, rec.Name) if err != nil { return fail(*r, fmt.Sprintf("failed to get the recordings: %s", err.Error())) } - - if recording.State != "STOPPED" { - return fail(*r, fmt.Sprintf("recording %s failed to stop: %s", recording.Name, err.Error())) + if rec.State != "STOPPED" { + return fail(*r, fmt.Sprintf("recording %s failed to stop: %s", rec.Name, err.Error())) } - r.Log += fmt.Sprintf("stopped the recording: %s\n", recording.Name) + r.Log += fmt.Sprintf("stopped the recording: %s\n", rec.Name) // Delete the recording - err = apiClient.Recordings().Delete(context.Background(), connectUrl, recording.Name) + err = apiClient.Recordings().Delete(context.Background(), connectUrl, rec.Name) + if err != nil { + return fail(*r, fmt.Sprintf("failed to delete the recording %s: %s", rec.Name, err.Error())) + } + r.Log += fmt.Sprintf("deleted the recording: %s\n", rec.Name) + + // View the current recording list after deleting one + recs, err = apiClient.Recordings().List(context.Background(), connectUrl) if err != nil { - return fail(*r, fmt.Sprintf("failed to delete the recording %s: %s", recording.Name, err.Error())) + return fail(*r, fmt.Sprintf("failed to list recordings: %s", err.Error())) } - r.Log += fmt.Sprintf("deleted the recording: %s\n", recording.Name) + r.Log += fmt.Sprintf("current list of recordings: %+v\n", recs) return *r } From 8a80ba703985b47fae471779cd0a180aa659cbe6 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 22 Dec 2023 06:37:44 -0800 Subject: [PATCH 23/39] test(scorecard): test generating report for a recording --- .../custom-scorecard-tests/Dockerfile.cross | 50 ++++++++++++++++++ internal/test/scorecard/clients.go | 52 ++++++++++++++----- internal/test/scorecard/common_utils.go | 2 +- internal/test/scorecard/tests.go | 12 +++-- internal/test/scorecard/types.go | 4 +- 5 files changed, 101 insertions(+), 19 deletions(-) create mode 100644 internal/images/custom-scorecard-tests/Dockerfile.cross diff --git a/internal/images/custom-scorecard-tests/Dockerfile.cross b/internal/images/custom-scorecard-tests/Dockerfile.cross new file mode 100644 index 00000000..e653a413 --- /dev/null +++ b/internal/images/custom-scorecard-tests/Dockerfile.cross @@ -0,0 +1,50 @@ +# Copyright The Cryostat Authors. +# +# 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. + +# Build the manager binary +FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.20 as builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY api/ api/ +COPY internal/images/custom-scorecard-tests/main.go internal/images/custom-scorecard-tests/main.go +COPY internal/test/scorecard/ internal/test/scorecard/ + +# Build +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} GO111MODULE=on go build -a -o cryostat-scorecard-tests \ + internal/images/custom-scorecard-tests/main.go + +FROM registry.access.redhat.com/ubi8/ubi-minimal:latest + +ENV TEST=/usr/local/bin/cryostat-scorecard-tests \ + USER_UID=1001 \ + USER_NAME=test + +COPY internal/images/custom-scorecard-tests/bin/user_setup /usr/local/bin/ +COPY internal/images/custom-scorecard-tests/bin/entrypoint /usr/local/bin/ +COPY --from=builder /workspace/cryostat-scorecard-tests /usr/local/bin/ +RUN /usr/local/bin/user_setup + +ENTRYPOINT ["/usr/local/bin/entrypoint"] + +USER ${USER_UID} diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index c80ba98d..1c58c525 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -263,6 +263,7 @@ func (client *RecordingClient) List(ctx context.Context, connectUrl string) ([]R if err != nil { return nil, err } + defer resp.Body.Close() if !StatusOK(resp.StatusCode) { return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) @@ -273,7 +274,6 @@ func (client *RecordingClient) List(ctx context.Context, connectUrl string) ([]R if err != nil { return nil, fmt.Errorf("failed to read response body: %s", err.Error()) } - defer resp.Body.Close() return recordings, nil } @@ -307,6 +307,7 @@ func (client *RecordingClient) Create(ctx context.Context, connectUrl string, op if err != nil { return nil, err } + defer resp.Body.Close() if !StatusOK(resp.StatusCode) { return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) @@ -318,8 +319,6 @@ func (client *RecordingClient) Create(ctx context.Context, connectUrl string, op return nil, fmt.Errorf("failed to read response body: %s", err.Error()) } - defer resp.Body.Close() - return recording, err } @@ -337,6 +336,7 @@ func (client *RecordingClient) Archive(ctx context.Context, connectUrl string, r if err != nil { return "", err } + defer resp.Body.Close() if !StatusOK(resp.StatusCode) { return "", fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) @@ -347,8 +347,6 @@ func (client *RecordingClient) Archive(ctx context.Context, connectUrl string, r return "", fmt.Errorf("failed to read response body: %s", err.Error()) } - defer resp.Body.Close() - return bodyAsString, nil } @@ -365,13 +363,12 @@ func (client *RecordingClient) Stop(ctx context.Context, connectUrl string, reco if err != nil { return err } + defer resp.Body.Close() if !StatusOK(resp.StatusCode) { return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } - defer resp.Body.Close() - return nil } @@ -386,13 +383,45 @@ func (client *RecordingClient) Delete(ctx context.Context, connectUrl string, re if err != nil { return err } + defer resp.Body.Close() + if !StatusOK(resp.StatusCode) { return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } + return nil +} + +func (client *RecordingClient) GenerateReport(ctx context.Context, connectUrl string, recordingName *Recording) (map[string]interface{}, error) { + reportURL := recordingName.ReportURL + + if len(reportURL) < 1 { + return nil, fmt.Errorf("report URL is not available") + } + + req, err := NewHttpRequest(ctx, http.MethodGet, reportURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create a REST request: %s", err.Error()) + } + req.Header.Add("Accept", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, err + } defer resp.Body.Close() - return nil + if !StatusOK(resp.StatusCode) { + return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + } + + report := make(map[string]interface{}, 0) + err = ReadJSON(resp, &report) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %s", err.Error()) + } + + return report, nil } type CredentialClient struct { @@ -412,25 +441,24 @@ func (client *CredentialClient) Create(ctx context.Context, credential *Credenti if err != nil { return err } + defer resp.Body.Close() if !StatusOK(resp.StatusCode) { return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } - defer resp.Body.Close() - return nil } func ReadJSON(resp *http.Response, result interface{}) error { body, err := io.ReadAll(resp.Body) if err != nil { - return nil + return err } err = json.Unmarshal(body, result) if err != nil { - return nil + return err } return nil } diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go index 5c0d279a..91ccbeb7 100644 --- a/internal/test/scorecard/common_utils.go +++ b/internal/test/scorecard/common_utils.go @@ -359,7 +359,7 @@ func waitTillCryostatReachable(base *url.URL, resources *TestResources) error { if !StatusOK(resp.StatusCode) { if resp.StatusCode == http.StatusServiceUnavailable { - r.Log += fmt.Sprintf("application is not yet reachable at %s\n", url.String()) + r.Log += fmt.Sprintf("application is not yet reachable at %s\n", base.String()) return false, nil // Try again } return false, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 003ba195..61323e67 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -147,7 +147,7 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh // Create a recording options := &RecordingCreateOptions{ - RecordingName: "scorecard-test-rec", + RecordingName: "scorecard_test_rec", Events: "template=ALL", Duration: 0, // Continuous ToDisk: true, @@ -167,8 +167,8 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh } r.Log += fmt.Sprintf("current list of recordings: %+v\n", recs) - // Allow the recording to run for 5s - time.Sleep(5 * time.Second) + // Allow the recording to run for 10s + time.Sleep(30 * time.Second) // Archive the recording archiveName, err := apiClient.Recordings().Archive(context.Background(), connectUrl, rec.Name) @@ -179,7 +179,11 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh // TODO: Fetch archives and output to log - // TODO: Generate a report + report, err := apiClient.Recordings().GenerateReport(context.Background(), connectUrl, rec) + if err != nil { + return fail(*r, fmt.Sprintf("failed to generate report for the recording: %s", err.Error())) + } + r.Log += fmt.Sprintf("generated report for the recording %s: %+v\n", rec.Name, report) // Stop the recording err = apiClient.Recordings().Stop(context.Background(), connectUrl, rec.Name) diff --git a/internal/test/scorecard/types.go b/internal/test/scorecard/types.go index 6c7f73d4..93ebe55e 100644 --- a/internal/test/scorecard/types.go +++ b/internal/test/scorecard/types.go @@ -60,9 +60,9 @@ func (cred *Credential) ToFormData() string { type Recording struct { DownloadURL string `json:"downloadUrl"` ReportURL string `json:"reportUrl"` - Id string `json:"id"` + Id uint32 `json:"id"` Name string `json:"name"` - StartTime int32 `json:"startTime"` + StartTime uint64 `json:"startTime"` State string `json:"state"` Duration int32 `json:"duration"` Continuous bool `json:"continuous"` From ab116be1d95efb512c3f4eb7c799f15d893d0173 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 22 Dec 2023 14:14:47 -0800 Subject: [PATCH 24/39] chore(scorecard): clean up --- .../custom-scorecard-tests/Dockerfile.cross | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 internal/images/custom-scorecard-tests/Dockerfile.cross diff --git a/internal/images/custom-scorecard-tests/Dockerfile.cross b/internal/images/custom-scorecard-tests/Dockerfile.cross deleted file mode 100644 index e653a413..00000000 --- a/internal/images/custom-scorecard-tests/Dockerfile.cross +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright The Cryostat Authors. -# -# 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. - -# Build the manager binary -FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.20 as builder -ARG TARGETOS -ARG TARGETARCH - -WORKDIR /workspace -# Copy the Go Modules manifests -COPY go.mod go.mod -COPY go.sum go.sum -# cache deps before building and copying source so that we don't need to re-download as much -# and so that source changes don't invalidate our downloaded layer -RUN go mod download - -# Copy the go source -COPY api/ api/ -COPY internal/images/custom-scorecard-tests/main.go internal/images/custom-scorecard-tests/main.go -COPY internal/test/scorecard/ internal/test/scorecard/ - -# Build -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} GO111MODULE=on go build -a -o cryostat-scorecard-tests \ - internal/images/custom-scorecard-tests/main.go - -FROM registry.access.redhat.com/ubi8/ubi-minimal:latest - -ENV TEST=/usr/local/bin/cryostat-scorecard-tests \ - USER_UID=1001 \ - USER_NAME=test - -COPY internal/images/custom-scorecard-tests/bin/user_setup /usr/local/bin/ -COPY internal/images/custom-scorecard-tests/bin/entrypoint /usr/local/bin/ -COPY --from=builder /workspace/cryostat-scorecard-tests /usr/local/bin/ -RUN /usr/local/bin/user_setup - -ENTRYPOINT ["/usr/local/bin/entrypoint"] - -USER ${USER_UID} From 4e3c531eb29aa73f9c3000fe7160976686a2556d Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 22 Dec 2023 14:18:36 -0800 Subject: [PATCH 25/39] test(scorecard): list archives in tests --- internal/test/scorecard/clients.go | 56 ++++++++++++++++++++++++++++++ internal/test/scorecard/tests.go | 6 +++- internal/test/scorecard/types.go | 28 +++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 1c58c525..bb9b0492 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -15,6 +15,7 @@ package scorecard import ( + "bytes" "context" "encoding/base64" "encoding/json" @@ -424,6 +425,61 @@ func (client *RecordingClient) GenerateReport(ctx context.Context, connectUrl st return report, nil } +func (client *RecordingClient) ListArchives(ctx context.Context, connectUrl string) ([]Archive, error) { + url := client.Base.JoinPath("/api/v2.2/graphql") + + query := &GraphQLQuery{ + Query: ` + query ArchivedRecordingsForTarget($connectUrl: String) { + archivedRecordings(filter: { sourceTarget: $connectUrl }) { + data { + name + downloadUrl + reportUrl + metadata { + labels + } + size + } + } + } + `, + Variables: map[string]string{ + connectUrl: connectUrl, + }, + } + queryJSON, err := query.ToJSON() + if err != nil { + return nil, fmt.Errorf("failed to construct graph query: %s", err.Error()) + } + + body := bytes.NewReader(queryJSON) + req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) + if err != nil { + return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "*/*") + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if !StatusOK(resp.StatusCode) { + return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + } + + graphQLResponse := &ArchiveGraphQLResponse{} + err = ReadJSON(resp, graphQLResponse) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %s", err.Error()) + } + + return graphQLResponse.Data.ArchivedRecordings.Data, nil +} + type CredentialClient struct { *commonCryostatRESTClient } diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 61323e67..8d54cf77 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -177,7 +177,11 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh } r.Log += fmt.Sprintf("archived the recording %s at: %s\n", rec.Name, archiveName) - // TODO: Fetch archives and output to log + archives, err := apiClient.Recordings().ListArchives(context.Background(), connectUrl) + if err != nil { + return fail(*r, fmt.Sprintf("failed to list archives: %s", err.Error())) + } + r.Log += fmt.Sprintf("current list of archives: %+v\n", archives) report, err := apiClient.Recordings().GenerateReport(context.Background(), connectUrl, rec) if err != nil { diff --git a/internal/test/scorecard/types.go b/internal/test/scorecard/types.go index 93ebe55e..9de14a16 100644 --- a/internal/test/scorecard/types.go +++ b/internal/test/scorecard/types.go @@ -15,6 +15,7 @@ package scorecard import ( + "encoding/json" "net/url" "strconv" ) @@ -71,7 +72,34 @@ type Recording struct { MaxAge int32 `json:"maxAge"` } +type Archive struct { + Name string + DownloadUrl string + ReportUrl string + Metadata struct { + Labels map[string]interface{} + } + Size int32 +} + type Target struct { ConnectUrl string `json:"connectUrl"` Alias string `json:"alias,omitempty"` } + +type GraphQLQuery struct { + Query string `json:"query"` + Variables map[string]string `json:"variables,omitempty"` +} + +func (query *GraphQLQuery) ToJSON() ([]byte, error) { + return json.Marshal(query) +} + +type ArchiveGraphQLResponse struct { + Data struct { + ArchivedRecordings struct { + Data []Archive `json:"data"` + } `json:"archivedRecordings"` + } `json:"data"` +} From 63e8824f66a5b0cd418a3674273aab7695ccc17b Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Fri, 22 Dec 2023 15:42:34 -0800 Subject: [PATCH 26/39] ci(scorecard): reconfigure ingress for kind --- .github/workflows/test-ci-reusable.yml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-ci-reusable.yml b/.github/workflows/test-ci-reusable.yml index a1c9be76..08c1d6e1 100644 --- a/.github/workflows/test-ci-reusable.yml +++ b/.github/workflows/test-ci-reusable.yml @@ -118,11 +118,26 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Kind cluster + uses: helm/kind-action@v1.8.0 + with: + config: .github/kind-config.yaml + - name: Set up Ingress Controller run: | - kind create cluster --config=".github/kind-config.yaml" -n ci-${{ github.run_id }} - # Enabling Ingress + # Install nginx ingress controller kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml - kubectl rollout status -w deployment/ingress-nginx-controller -n ingress-nginx --timeout 5m + kubectl rollout status -w \ + deployment/ingress-nginx-controller \ + -n ingress-nginx --timeout 5m + + # Lower the number of worker processes + kubectl patch cm/ingress-nginx-controller \ + --type merge \ + -p '{"data":{"worker-processes":"1"}}' \ + -n ingress-nginx + + # Modify /etc/hosts to resolve hostnames + ip_address=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' kind-control-plane) + echo "$ip_address testing.cryostat" | sudo tee -a /etc/hosts - name: Install Operator Lifecycle Manager run: curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/v0.24.0/install.sh | bash -s v0.24.0 - name: Install Cert Manager @@ -140,8 +155,6 @@ jobs: SCORECARD_REGISTRY_PASSWORD="${{ secrets.GITHUB_TOKEN }}" \ BUNDLE_IMG="${{ steps.push-bundle-to-ghcr.outputs.registry-path }}" \ make test-scorecard - - name: Clean up Kind cluster - run: kind delete cluster -n ci-${{ github.run_id }} - name: Set latest commit status as ${{ job.status }} uses: myrotvorets/set-commit-status-action@master if: always() From d1e3f42e9779349a22affbf2dc72f531c0ae1416 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Sun, 24 Dec 2023 09:32:54 -0800 Subject: [PATCH 27/39] ci(k8s): correct cluster name --- .github/workflows/test-ci-reusable.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-ci-reusable.yml b/.github/workflows/test-ci-reusable.yml index 08c1d6e1..860971ac 100644 --- a/.github/workflows/test-ci-reusable.yml +++ b/.github/workflows/test-ci-reusable.yml @@ -121,6 +121,9 @@ jobs: uses: helm/kind-action@v1.8.0 with: config: .github/kind-config.yaml + cluster_name: ci-${{ github.run_id }} + wait: 1m + ignore_failed_clean: true - name: Set up Ingress Controller run: | # Install nginx ingress controller @@ -136,7 +139,7 @@ jobs: -n ingress-nginx # Modify /etc/hosts to resolve hostnames - ip_address=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' kind-control-plane) + ip_address=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ci-${{ github.run_id }}-control-plane) echo "$ip_address testing.cryostat" | sudo tee -a /etc/hosts - name: Install Operator Lifecycle Manager run: curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/v0.24.0/install.sh | bash -s v0.24.0 From eddc69f0c3ce5b3e56adb3b1df53c1c48aa38880 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Sun, 24 Dec 2023 11:27:43 -0800 Subject: [PATCH 28/39] test(scorecard): use role instead of clusterrole for oauth rules --- .../rbac/scorecard_role.yaml | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml b/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml index a59a079f..d350e646 100644 --- a/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml +++ b/internal/images/custom-scorecard-tests/rbac/scorecard_role.yaml @@ -59,47 +59,6 @@ rules: - secrets verbs: - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: cryostat-scorecard -rules: -- apiGroups: - - config.openshift.io - resources: - - operatorhubs - verbs: - - patch -- apiGroups: - - config.openshift.io - resources: - - clusterversions - verbs: - - get - - list -- apiGroups: - - operators.coreos.com - resources: - - operatorgroups - verbs: - - create -- apiGroups: - - operators.coreos.com - resources: - - subscriptions - - clusterserviceversions - verbs: - - create - - get - - list - - watch -- apiGroups: - - "" - resources: - - namespaces - verbs: - - create # Permissions for default OAuth configurations - apiGroups: - operator.cryostat.io @@ -143,3 +102,44 @@ rules: - statefulsets verbs: - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cryostat-scorecard +rules: +- apiGroups: + - config.openshift.io + resources: + - operatorhubs + verbs: + - patch +- apiGroups: + - config.openshift.io + resources: + - clusterversions + verbs: + - get + - list +- apiGroups: + - operators.coreos.com + resources: + - operatorgroups + verbs: + - create +- apiGroups: + - operators.coreos.com + resources: + - subscriptions + - clusterserviceversions + verbs: + - create + - get + - list + - watch +- apiGroups: + - "" + resources: + - namespaces + verbs: + - create From 8b7c29f4e3d5e46517d241252b0b12fb06176d72 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 15 Jan 2024 12:28:28 -0800 Subject: [PATCH 29/39] test(scorecard): parse health response for additional checks --- internal/test/scorecard/common_utils.go | 16 +++++++++++++-- internal/test/scorecard/tests.go | 2 +- internal/test/scorecard/types.go | 26 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go index 91ccbeb7..94c52db8 100644 --- a/internal/test/scorecard/common_utils.go +++ b/internal/test/scorecard/common_utils.go @@ -331,12 +331,12 @@ func createAndWaitTillCryostatAvailable(cr *operatorv1beta1.Cryostat, resources logError(r, fmt.Sprintf("application URL not found in CR: %s", err.Error())) return nil, err } - r.Log += fmt.Sprintf("application is ready at %s\n", cr.Status.ApplicationURL) + r.Log += fmt.Sprintf("application is available at %s\n", cr.Status.ApplicationURL) return cr, nil } -func waitTillCryostatReachable(base *url.URL, resources *TestResources) error { +func waitTillCryostatReady(base *url.URL, resources *TestResources) error { client := NewHttpClient() r := resources.TestResult @@ -357,6 +357,12 @@ func waitTillCryostatReachable(base *url.URL, resources *TestResources) error { } defer resp.Body.Close() + health := &HealthResponse{} + err = ReadJSON(resp, health) + if err != nil { + return false, fmt.Errorf("failed to read response body: %s", err.Error()) + } + if !StatusOK(resp.StatusCode) { if resp.StatusCode == http.StatusServiceUnavailable { r.Log += fmt.Sprintf("application is not yet reachable at %s\n", base.String()) @@ -365,6 +371,12 @@ func waitTillCryostatReachable(base *url.URL, resources *TestResources) error { return false, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } + if err = health.Ready(); err != nil { + r.Log += fmt.Sprintf("application is not yet ready: %s", err.Error()) + return false, nil // Try again + } + + r.Log += fmt.Sprintf("application is ready at %s\n", base.String()) return true, nil }) diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 8d54cf77..04b5f0bd 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -94,7 +94,7 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh return fail(*r, fmt.Sprintf("application URL is invalid: %s", err.Error())) } - err = waitTillCryostatReachable(base, tr) + err = waitTillCryostatReady(base, tr) if err != nil { return fail(*r, fmt.Sprintf("failed to reach the application: %s", err.Error())) } diff --git a/internal/test/scorecard/types.go b/internal/test/scorecard/types.go index 9de14a16..3eb57816 100644 --- a/internal/test/scorecard/types.go +++ b/internal/test/scorecard/types.go @@ -16,10 +16,36 @@ package scorecard import ( "encoding/json" + "errors" "net/url" "strconv" ) +type HealthResponse struct { + CryostatVersion string `json:"cryostatVersion"` + DashboardAvailable bool `json:"dashboardAvailable"` + DashboardConfigured bool `json:"dashboardConfigured"` + DataSourceAvailable bool `json:"datasourceAvailable"` + DataSourceConfigured bool `json:"datasourceConfigured"` + ReportsAvailable bool `json:"reportsAvailable"` + ReportsConfigured bool `json:"reportsConfigured"` +} + +func (health *HealthResponse) Ready() error { + if !health.DashboardAvailable { + return errors.New("dashboard is not available") + } + + if !health.DataSourceAvailable { + return errors.New("datasource is not available") + } + + if !health.ReportsAvailable { + return errors.New("report is not available") + } + return nil +} + type RecordingCreateOptions struct { RecordingName string Events string From 4a99137e2ac7374072117ee8cd8c86b0b2f6c89b Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 15 Jan 2024 12:32:25 -0800 Subject: [PATCH 30/39] chore(scorecard): add missing newline in logs --- internal/test/scorecard/common_utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go index 94c52db8..3a53bbfe 100644 --- a/internal/test/scorecard/common_utils.go +++ b/internal/test/scorecard/common_utils.go @@ -372,7 +372,7 @@ func waitTillCryostatReady(base *url.URL, resources *TestResources) error { } if err = health.Ready(); err != nil { - r.Log += fmt.Sprintf("application is not yet ready: %s", err.Error()) + r.Log += fmt.Sprintf("application is not yet ready: %s\n", err.Error()) return false, nil // Try again } From c2f4f534774f2e484315d698c801b32462ac4702 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Wed, 31 Jan 2024 13:13:01 -0800 Subject: [PATCH 31/39] chore(scorecard): check status code before parsing body in health check --- internal/test/scorecard/common_utils.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go index 3a53bbfe..65ece167 100644 --- a/internal/test/scorecard/common_utils.go +++ b/internal/test/scorecard/common_utils.go @@ -357,12 +357,6 @@ func waitTillCryostatReady(base *url.URL, resources *TestResources) error { } defer resp.Body.Close() - health := &HealthResponse{} - err = ReadJSON(resp, health) - if err != nil { - return false, fmt.Errorf("failed to read response body: %s", err.Error()) - } - if !StatusOK(resp.StatusCode) { if resp.StatusCode == http.StatusServiceUnavailable { r.Log += fmt.Sprintf("application is not yet reachable at %s\n", base.String()) @@ -371,6 +365,12 @@ func waitTillCryostatReady(base *url.URL, resources *TestResources) error { return false, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) } + health := &HealthResponse{} + err = ReadJSON(resp, health) + if err != nil { + return false, fmt.Errorf("failed to read response body: %s", err.Error()) + } + if err = health.Ready(); err != nil { r.Log += fmt.Sprintf("application is not yet ready: %s\n", err.Error()) return false, nil // Try again From b7cca98a1c28d24a4eff8a61836892912a2416e6 Mon Sep 17 00:00:00 2001 From: Ming Yu Wang <90855268+mwangggg@users.noreply.github.com> Date: Thu, 29 Feb 2024 11:36:38 -0500 Subject: [PATCH 32/39] test(scorecard): add custom target discovery to recording scorecard test --- internal/test/scorecard/clients.go | 32 ++++++++++++++++++++++++++++-- internal/test/scorecard/tests.go | 31 ++++++++++------------------- internal/test/scorecard/types.go | 15 ++++++++++++++ 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index bb9b0492..3659f357 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -231,6 +231,7 @@ func (client *TargetClient) List(ctx context.Context) ([]Target, error) { if err != nil { return nil, err } + defer resp.Body.Close() if !StatusOK(resp.StatusCode) { return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) @@ -242,9 +243,36 @@ func (client *TargetClient) List(ctx context.Context) ([]Target, error) { return nil, fmt.Errorf("failed to read response body: %s", err.Error()) } + return targets, nil +} + +func (client *TargetClient) Create(ctx context.Context, options *Target) (*Target, error) { + url := client.Base.JoinPath("/api/v2/targets") + body := strings.NewReader(options.ToFormData()) + req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) + if err != nil { + return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Accept", "*/*") + + resp, err := client.Do(req) + if err != nil { + return nil, err + } defer resp.Body.Close() - return targets, nil + if !StatusOK(resp.StatusCode) { + return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + } + + targetResp := &CustomTargetResponse{} + err = ReadJSON(resp, targetResp) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %s", err.Error()) + } + + return targetResp.Data.Result, nil } // Client for Cryostat Recording resources @@ -529,7 +557,7 @@ func ReadString(resp *http.Response) (string, error) { func ReadError(resp *http.Response) string { body, _ := ReadString(resp) - return string(body) + return body } func NewHttpClient() *http.Client { diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 04b5f0bd..2860aa65 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -23,7 +23,6 @@ import ( scapiv1alpha3 "github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3" apimanifests "github.com/operator-framework/api/pkg/manifests" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" ) const ( @@ -73,6 +72,7 @@ func CryostatCRTest(bundle *apimanifests.Bundle, namespace string, openShiftCert return *r } +// TODO add a built in discovery test too func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openShiftCertManager bool) scapiv1alpha3.TestResult { tr := newTestResources(CryostatRecordingTestName) r := tr.TestResult @@ -101,27 +101,16 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh apiClient := NewCryostatRESTClientset(base) - // Get a target for test - var target Target - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - err = wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { - targets, err := apiClient.Targets().List(context.Background()) - if err != nil { - logError(r, fmt.Sprintf("failed to list discovered targets: %s", err.Error())) - return false, err - } - if len(targets) == 0 { - r.Log += "no target is yet discovered\n" - return false, nil // Try again - } - target = targets[0] // Cryostat - r.Log += fmt.Sprintf("found a target: %+v\n", target) - return true, nil - }) - if err != nil { - return fail(*r, fmt.Sprintf("failed to get a target for test: %s", err.Error())) + // Create a custom target for test + targetOptions := &Target{ + ConnectUrl: "service:jmx:rmi:///jndi/rmi://localhost:0/jmxrmi", + Alias: "customTarget", + } + target, err := apiClient.Targets().Create(context.Background(), targetOptions) + if err != nil { + return fail(*r, fmt.Sprintf("failed to create a target: %s", err.Error())) } + r.Log += fmt.Sprintf("created a custom target: %+v\n", target) connectUrl := target.ConnectUrl jmxSecretName := CryostatRecordingTestName + "-jmx-auth" diff --git a/internal/test/scorecard/types.go b/internal/test/scorecard/types.go index 3eb57816..17be1136 100644 --- a/internal/test/scorecard/types.go +++ b/internal/test/scorecard/types.go @@ -108,11 +108,26 @@ type Archive struct { Size int32 } +type CustomTargetResponse struct { + Data struct { + Result *Target `json:"result"` + } `json:"data"` +} + type Target struct { ConnectUrl string `json:"connectUrl"` Alias string `json:"alias,omitempty"` } +func (opts *Target) ToFormData() string { + formData := &url.Values{} + + formData.Add("connectUrl", opts.ConnectUrl) + formData.Add("alias", opts.Alias) + + return formData.Encode() +} + type GraphQLQuery struct { Query string `json:"query"` Variables map[string]string `json:"variables,omitempty"` From 097aa8620924dadb0530e7803ee2e67892634f39 Mon Sep 17 00:00:00 2001 From: Ming Wang Date: Thu, 29 Feb 2024 13:19:50 -0500 Subject: [PATCH 33/39] add EOF wait and resp headers --- internal/test/scorecard/clients.go | 13 +++++++++++-- internal/test/scorecard/tests.go | 23 +++++++++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 3659f357..050a219d 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -253,7 +253,6 @@ func (client *TargetClient) Create(ctx context.Context, options *Target) (*Targe if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Accept", "*/*") resp, err := client.Do(req) @@ -263,7 +262,7 @@ func (client *TargetClient) Create(ctx context.Context, options *Target) (*Targe defer resp.Body.Close() if !StatusOK(resp.StatusCode) { - return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + return nil, fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) } targetResp := &CustomTargetResponse{} @@ -555,6 +554,16 @@ func ReadString(resp *http.Response) (string, error) { return string(body), nil } +func ReadHeader(resp *http.Response) string { + var header string + for name, value := range resp.Header { + for _, h := range value { + header += fmt.Sprintf("%s: %s\n", name, h) + } + } + return header +} + func ReadError(resp *http.Response) string { body, _ := ReadString(resp) return body diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 2860aa65..8a035a99 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -16,13 +16,16 @@ package scorecard import ( "context" + "errors" "fmt" + "io" "net/url" "time" scapiv1alpha3 "github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3" apimanifests "github.com/operator-framework/api/pkg/manifests" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" ) const ( @@ -160,11 +163,23 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh time.Sleep(30 * time.Second) // Archive the recording - archiveName, err := apiClient.Recordings().Archive(context.Background(), connectUrl, rec.Name) - if err != nil { - return fail(*r, fmt.Sprintf("failed to archive the recording: %s", err.Error())) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + err = wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { + archiveName, err := apiClient.Recordings().Archive(context.Background(), connectUrl, rec.Name) + if errors.Is(err, io.EOF) { + r.Log += fmt.Sprintf("archiving recording resulted in EOF: %s, trying again", err.Error()) + return false, nil + } + if err != nil { + return true, fmt.Errorf("failed to archive the recording: %s", err.Error()) + } + r.Log += fmt.Sprintf("archived the recording %s at: %s\n", rec.Name, archiveName) + return true, nil + }) + if err != nil { + return fail(*r, err.Error()) } - r.Log += fmt.Sprintf("archived the recording %s at: %s\n", rec.Name, archiveName) archives, err := apiClient.Recordings().ListArchives(context.Background(), connectUrl) if err != nil { From a047f628befedbd4811ca266b2f1cf26f636dd32 Mon Sep 17 00:00:00 2001 From: Ming Wang Date: Thu, 29 Feb 2024 13:21:24 -0500 Subject: [PATCH 34/39] add resp headers --- internal/test/scorecard/clients.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 050a219d..c12ec804 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -234,7 +234,7 @@ func (client *TargetClient) List(ctx context.Context) ([]Target, error) { defer resp.Body.Close() if !StatusOK(resp.StatusCode) { - return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + return nil, fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) } targets := make([]Target, 0) @@ -294,7 +294,7 @@ func (client *RecordingClient) List(ctx context.Context, connectUrl string) ([]R defer resp.Body.Close() if !StatusOK(resp.StatusCode) { - return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + return nil, fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) } recordings := make([]Recording, 0) @@ -338,7 +338,7 @@ func (client *RecordingClient) Create(ctx context.Context, connectUrl string, op defer resp.Body.Close() if !StatusOK(resp.StatusCode) { - return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + return nil, fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) } recording := &Recording{} @@ -367,7 +367,7 @@ func (client *RecordingClient) Archive(ctx context.Context, connectUrl string, r defer resp.Body.Close() if !StatusOK(resp.StatusCode) { - return "", fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + return "", fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) } bodyAsString, err := ReadString(resp) @@ -394,7 +394,7 @@ func (client *RecordingClient) Stop(ctx context.Context, connectUrl string, reco defer resp.Body.Close() if !StatusOK(resp.StatusCode) { - return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + return fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) } return nil @@ -414,7 +414,7 @@ func (client *RecordingClient) Delete(ctx context.Context, connectUrl string, re defer resp.Body.Close() if !StatusOK(resp.StatusCode) { - return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + return fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) } return nil @@ -440,7 +440,7 @@ func (client *RecordingClient) GenerateReport(ctx context.Context, connectUrl st defer resp.Body.Close() if !StatusOK(resp.StatusCode) { - return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + return nil, fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) } report := make(map[string]interface{}, 0) @@ -495,7 +495,7 @@ func (client *RecordingClient) ListArchives(ctx context.Context, connectUrl stri defer resp.Body.Close() if !StatusOK(resp.StatusCode) { - return nil, fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + return nil, fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) } graphQLResponse := &ArchiveGraphQLResponse{} @@ -527,7 +527,7 @@ func (client *CredentialClient) Create(ctx context.Context, credential *Credenti defer resp.Body.Close() if !StatusOK(resp.StatusCode) { - return fmt.Errorf("API request failed with status code %d: %s", resp.StatusCode, ReadError(resp)) + return fmt.Errorf("API request failed with status code: %d, response body: %s, and headers:\n%s", resp.StatusCode, ReadError(resp), ReadHeader(resp)) } return nil From 170aabf51277f02efcdd23f8098a38b90a9d58c6 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Thu, 29 Feb 2024 11:03:17 -0800 Subject: [PATCH 35/39] chore(client): configure all clients to send safe requests --- internal/test/scorecard/clients.go | 43 ++++++++++++++++++++++-------- internal/test/scorecard/tests.go | 23 +++------------- internal/test/scorecard/types.go | 6 ++--- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index c12ec804..8135a7d5 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -19,16 +19,19 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "net/http" "net/url" "strings" + "time" operatorv1beta1 "github.com/cryostatio/cryostat-operator/api/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" @@ -227,7 +230,7 @@ func (client *TargetClient) List(ctx context.Context) ([]Target, error) { } req.Header.Add("Accept", "*/*") - resp, err := client.Do(req) + resp, err := SendRequest(ctx, client.Client, req) if err != nil { return nil, err } @@ -255,7 +258,7 @@ func (client *TargetClient) Create(ctx context.Context, options *Target) (*Targe } req.Header.Add("Accept", "*/*") - resp, err := client.Do(req) + resp, err := SendRequest(ctx, client.Client, req) if err != nil { return nil, err } @@ -287,7 +290,7 @@ func (client *RecordingClient) List(ctx context.Context, connectUrl string) ([]R } req.Header.Add("Accept", "*/*") - resp, err := client.Do(req) + resp, err := SendRequest(ctx, client.Client, req) if err != nil { return nil, err } @@ -331,7 +334,7 @@ func (client *RecordingClient) Create(ctx context.Context, connectUrl string, op req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Accept", "*/*") - resp, err := client.Do(req) + resp, err := SendRequest(ctx, client.Client, req) if err != nil { return nil, err } @@ -360,7 +363,7 @@ func (client *RecordingClient) Archive(ctx context.Context, connectUrl string, r req.Header.Add("Content-Type", "text/plain") req.Header.Add("Accept", "*/*") - resp, err := client.Do(req) + resp, err := SendRequest(ctx, client.Client, req) if err != nil { return "", err } @@ -387,7 +390,7 @@ func (client *RecordingClient) Stop(ctx context.Context, connectUrl string, reco } req.Header.Add("Content-Type", "text/plain") - resp, err := client.Do(req) + resp, err := SendRequest(ctx, client.Client, req) if err != nil { return err } @@ -407,7 +410,7 @@ func (client *RecordingClient) Delete(ctx context.Context, connectUrl string, re return fmt.Errorf("failed to create a REST request: %s", err.Error()) } - resp, err := client.Do(req) + resp, err := SendRequest(ctx, client.Client, req) if err != nil { return err } @@ -433,7 +436,7 @@ func (client *RecordingClient) GenerateReport(ctx context.Context, connectUrl st } req.Header.Add("Accept", "application/json") - resp, err := client.Do(req) + resp, err := SendRequest(ctx, client.Client, req) if err != nil { return nil, err } @@ -488,7 +491,7 @@ func (client *RecordingClient) ListArchives(ctx context.Context, connectUrl stri req.Header.Add("Content-Type", "application/json") req.Header.Add("Accept", "*/*") - resp, err := client.Do(req) + resp, err := SendRequest(ctx, client.Client, req) if err != nil { return nil, err } @@ -520,7 +523,7 @@ func (client *CredentialClient) Create(ctx context.Context, credential *Credenti } req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - resp, err := client.Do(req) + resp, err := SendRequest(ctx, client.Client, req) if err != nil { return err } @@ -555,7 +558,7 @@ func ReadString(resp *http.Response) (string, error) { } func ReadHeader(resp *http.Response) string { - var header string + header := "" for name, value := range resp.Header { for _, h := range value { header += fmt.Sprintf("%s: %s\n", name, h) @@ -599,3 +602,21 @@ func NewHttpRequest(ctx context.Context, method string, url string, body io.Read func StatusOK(statusCode int) bool { return statusCode >= 200 && statusCode < 300 } + +func SendRequest(ctx context.Context, httpClient *http.Client, req *http.Request) (*http.Response, error) { + var response *http.Response + err := wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { + resp, err := httpClient.Do(req) + if err != nil { + // Retry when connection is closed. + if errors.Is(err, io.EOF) { + return false, nil + } + return false, err + } + response = resp + return true, nil + }) + + return response, err +} diff --git a/internal/test/scorecard/tests.go b/internal/test/scorecard/tests.go index 8a035a99..2860aa65 100644 --- a/internal/test/scorecard/tests.go +++ b/internal/test/scorecard/tests.go @@ -16,16 +16,13 @@ package scorecard import ( "context" - "errors" "fmt" - "io" "net/url" "time" scapiv1alpha3 "github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3" apimanifests "github.com/operator-framework/api/pkg/manifests" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" ) const ( @@ -163,23 +160,11 @@ func CryostatRecordingTest(bundle *apimanifests.Bundle, namespace string, openSh time.Sleep(30 * time.Second) // Archive the recording - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - err = wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { - archiveName, err := apiClient.Recordings().Archive(context.Background(), connectUrl, rec.Name) - if errors.Is(err, io.EOF) { - r.Log += fmt.Sprintf("archiving recording resulted in EOF: %s, trying again", err.Error()) - return false, nil - } - if err != nil { - return true, fmt.Errorf("failed to archive the recording: %s", err.Error()) - } - r.Log += fmt.Sprintf("archived the recording %s at: %s\n", rec.Name, archiveName) - return true, nil - }) - if err != nil { - return fail(*r, err.Error()) + archiveName, err := apiClient.Recordings().Archive(context.Background(), connectUrl, rec.Name) + if err != nil { + return fail(*r, fmt.Sprintf("failed to archive the recording: %s", err.Error())) } + r.Log += fmt.Sprintf("archived the recording %s at: %s\n", rec.Name, archiveName) archives, err := apiClient.Recordings().ListArchives(context.Background(), connectUrl) if err != nil { diff --git a/internal/test/scorecard/types.go b/internal/test/scorecard/types.go index 17be1136..7e34ff4a 100644 --- a/internal/test/scorecard/types.go +++ b/internal/test/scorecard/types.go @@ -119,11 +119,11 @@ type Target struct { Alias string `json:"alias,omitempty"` } -func (opts *Target) ToFormData() string { +func (target *Target) ToFormData() string { formData := &url.Values{} - formData.Add("connectUrl", opts.ConnectUrl) - formData.Add("alias", opts.Alias) + formData.Add("connectUrl", target.ConnectUrl) + formData.Add("alias", target.Alias) return formData.Encode() } From f8f5c9c800cc34fd798f14ca5d32b437321dc834 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Thu, 29 Feb 2024 11:12:47 -0800 Subject: [PATCH 36/39] fix(clients): add missing content-type header --- internal/test/scorecard/clients.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 8135a7d5..2a5d20d0 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -256,6 +256,7 @@ func (client *TargetClient) Create(ctx context.Context, options *Target) (*Targe if err != nil { return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Accept", "*/*") resp, err := SendRequest(ctx, client.Client, req) From 71a5719b54c589966a6da28aeec15360c5fc87fc Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 4 Mar 2024 16:42:00 -0800 Subject: [PATCH 37/39] fix(scorecard): add missing test name in help message --- internal/images/custom-scorecard-tests/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/images/custom-scorecard-tests/main.go b/internal/images/custom-scorecard-tests/main.go index 96684530..62808a82 100644 --- a/internal/images/custom-scorecard-tests/main.go +++ b/internal/images/custom-scorecard-tests/main.go @@ -79,6 +79,7 @@ func printValidTests() []scapiv1alpha3.TestResult { str := fmt.Sprintf("valid tests for this image include: %s", strings.Join([]string{ tests.OperatorInstallTestName, tests.CryostatCRTestName, + tests.CryostatRecordingTestName, }, ",")) result.Errors = append(result.Errors, str) From fa5a6769c123e9c300a5f384053b12828ea7d2d7 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 4 Mar 2024 17:19:32 -0800 Subject: [PATCH 38/39] chore(client): create new http requests when retrying --- internal/test/scorecard/clients.go | 129 ++++++++++-------------- internal/test/scorecard/common_utils.go | 2 +- 2 files changed, 57 insertions(+), 74 deletions(-) diff --git a/internal/test/scorecard/clients.go b/internal/test/scorecard/clients.go index 2a5d20d0..ffe3ad79 100644 --- a/internal/test/scorecard/clients.go +++ b/internal/test/scorecard/clients.go @@ -15,7 +15,6 @@ package scorecard import ( - "bytes" "context" "encoding/base64" "encoding/json" @@ -224,13 +223,10 @@ type TargetClient struct { func (client *TargetClient) List(ctx context.Context) ([]Target, error) { url := client.Base.JoinPath("/api/v1/targets") - req, err := NewHttpRequest(ctx, http.MethodGet, url.String(), nil) - if err != nil { - return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) - } - req.Header.Add("Accept", "*/*") + header := make(http.Header) + header.Add("Accept", "*/*") - resp, err := SendRequest(ctx, client.Client, req) + resp, err := SendRequest(ctx, client.Client, http.MethodGet, url.String(), nil, header) if err != nil { return nil, err } @@ -251,15 +247,12 @@ func (client *TargetClient) List(ctx context.Context) ([]Target, error) { func (client *TargetClient) Create(ctx context.Context, options *Target) (*Target, error) { url := client.Base.JoinPath("/api/v2/targets") - body := strings.NewReader(options.ToFormData()) - req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) - if err != nil { - return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) - } - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Accept", "*/*") + header := make(http.Header) + header.Add("Content-Type", "application/x-www-form-urlencoded") + header.Add("Accept", "*/*") + body := options.ToFormData() - resp, err := SendRequest(ctx, client.Client, req) + resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), &body, header) if err != nil { return nil, err } @@ -285,13 +278,10 @@ type RecordingClient struct { func (client *RecordingClient) List(ctx context.Context, connectUrl string) ([]Recording, error) { url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings", url.PathEscape(connectUrl))) - req, err := NewHttpRequest(ctx, http.MethodGet, url.String(), nil) - if err != nil { - return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) - } - req.Header.Add("Accept", "*/*") + header := make(http.Header) + header.Add("Accept", "*/*") - resp, err := SendRequest(ctx, client.Client, req) + resp, err := SendRequest(ctx, client.Client, http.MethodGet, url.String(), nil, header) if err != nil { return nil, err } @@ -327,15 +317,12 @@ func (client *RecordingClient) Get(ctx context.Context, connectUrl string, recor func (client *RecordingClient) Create(ctx context.Context, connectUrl string, options *RecordingCreateOptions) (*Recording, error) { url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings", url.PathEscape(connectUrl))) - body := strings.NewReader(options.ToFormData()) - req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) - if err != nil { - return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) - } - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Accept", "*/*") + body := options.ToFormData() + header := make(http.Header) + header.Add("Content-Type", "application/x-www-form-urlencoded") + header.Add("Accept", "*/*") - resp, err := SendRequest(ctx, client.Client, req) + resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), &body, header) if err != nil { return nil, err } @@ -356,15 +343,12 @@ func (client *RecordingClient) Create(ctx context.Context, connectUrl string, op func (client *RecordingClient) Archive(ctx context.Context, connectUrl string, recordingName string) (string, error) { url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), url.PathEscape(recordingName))) - body := strings.NewReader("SAVE") - req, err := NewHttpRequest(ctx, http.MethodPatch, url.String(), body) - if err != nil { - return "", fmt.Errorf("failed to create a REST request: %s", err.Error()) - } - req.Header.Add("Content-Type", "text/plain") - req.Header.Add("Accept", "*/*") + body := "SAVE" + header := make(http.Header) + header.Add("Content-Type", "text/plain") + header.Add("Accept", "*/*") - resp, err := SendRequest(ctx, client.Client, req) + resp, err := SendRequest(ctx, client.Client, http.MethodPatch, url.String(), &body, header) if err != nil { return "", err } @@ -384,14 +368,12 @@ func (client *RecordingClient) Archive(ctx context.Context, connectUrl string, r func (client *RecordingClient) Stop(ctx context.Context, connectUrl string, recordingName string) error { url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), url.PathEscape(recordingName))) - body := strings.NewReader("STOP") - req, err := NewHttpRequest(ctx, http.MethodPatch, url.String(), body) - if err != nil { - return fmt.Errorf("failed to create a REST request: %s", err.Error()) - } - req.Header.Add("Content-Type", "text/plain") + body := "STOP" + header := make(http.Header) + header.Add("Content-Type", "text/plain") + header.Add("Accept", "*/*") - resp, err := SendRequest(ctx, client.Client, req) + resp, err := SendRequest(ctx, client.Client, http.MethodPatch, url.String(), &body, header) if err != nil { return err } @@ -406,12 +388,9 @@ func (client *RecordingClient) Stop(ctx context.Context, connectUrl string, reco func (client *RecordingClient) Delete(ctx context.Context, connectUrl string, recordingName string) error { url := client.Base.JoinPath(fmt.Sprintf("/api/v1/targets/%s/recordings/%s", url.PathEscape(connectUrl), url.PathEscape(recordingName))) - req, err := NewHttpRequest(ctx, http.MethodDelete, url.String(), nil) - if err != nil { - return fmt.Errorf("failed to create a REST request: %s", err.Error()) - } + header := make(http.Header) - resp, err := SendRequest(ctx, client.Client, req) + resp, err := SendRequest(ctx, client.Client, http.MethodDelete, url.String(), nil, header) if err != nil { return err } @@ -431,13 +410,10 @@ func (client *RecordingClient) GenerateReport(ctx context.Context, connectUrl st return nil, fmt.Errorf("report URL is not available") } - req, err := NewHttpRequest(ctx, http.MethodGet, reportURL, nil) - if err != nil { - return nil, fmt.Errorf("failed to create a REST request: %s", err.Error()) - } - req.Header.Add("Accept", "application/json") + header := make(http.Header) + header.Add("Accept", "application/json") - resp, err := SendRequest(ctx, client.Client, req) + resp, err := SendRequest(ctx, client.Client, http.MethodGet, reportURL, nil, header) if err != nil { return nil, err } @@ -483,16 +459,13 @@ func (client *RecordingClient) ListArchives(ctx context.Context, connectUrl stri if err != nil { return nil, fmt.Errorf("failed to construct graph query: %s", err.Error()) } + body := string(queryJSON) - body := bytes.NewReader(queryJSON) - req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) - if err != nil { - return nil, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Accept", "*/*") + header := make(http.Header) + header.Add("Content-Type", "application/json") + header.Add("Accept", "*/*") - resp, err := SendRequest(ctx, client.Client, req) + resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), &body, header) if err != nil { return nil, err } @@ -517,14 +490,11 @@ type CredentialClient struct { func (client *CredentialClient) Create(ctx context.Context, credential *Credential) error { url := client.Base.JoinPath("/api/v2.2/credentials") - body := strings.NewReader(credential.ToFormData()) - req, err := NewHttpRequest(ctx, http.MethodPost, url.String(), body) - if err != nil { - return fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) - } - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + body := credential.ToFormData() + header := make(http.Header) + header.Add("Content-Type", "application/x-www-form-urlencoded") - resp, err := SendRequest(ctx, client.Client, req) + resp, err := SendRequest(ctx, client.Client, http.MethodPost, url.String(), &body, header) if err != nil { return err } @@ -586,11 +556,18 @@ func NewHttpClient() *http.Client { return client } -func NewHttpRequest(ctx context.Context, method string, url string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequestWithContext(ctx, method, url, body) +func NewHttpRequest(ctx context.Context, method string, url string, body *string, header http.Header) (*http.Request, error) { + var reqBody io.Reader + if body != nil { + reqBody = strings.NewReader(*body) + } + req, err := http.NewRequestWithContext(ctx, method, url, reqBody) if err != nil { return nil, err } + if header != nil { + req.Header = header + } // Authentication is only enabled on OCP. Ignored on k8s. config, err := rest.InClusterConfig() if err != nil { @@ -604,9 +581,15 @@ func StatusOK(statusCode int) bool { return statusCode >= 200 && statusCode < 300 } -func SendRequest(ctx context.Context, httpClient *http.Client, req *http.Request) (*http.Response, error) { +func SendRequest(ctx context.Context, httpClient *http.Client, method string, url string, body *string, header http.Header) (*http.Response, error) { var response *http.Response err := wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { + // Create a new request + req, err := NewHttpRequest(ctx, method, url, body, header) + if err != nil { + return false, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) + } + resp, err := httpClient.Do(req) if err != nil { // Retry when connection is closed. diff --git a/internal/test/scorecard/common_utils.go b/internal/test/scorecard/common_utils.go index 65ece167..0b8daa58 100644 --- a/internal/test/scorecard/common_utils.go +++ b/internal/test/scorecard/common_utils.go @@ -345,7 +345,7 @@ func waitTillCryostatReady(base *url.URL, resources *TestResources) error { err := wait.PollImmediateUntilWithContext(ctx, time.Second, func(ctx context.Context) (done bool, err error) { url := base.JoinPath("/health") - req, err := NewHttpRequest(ctx, http.MethodGet, url.String(), nil) + req, err := NewHttpRequest(ctx, http.MethodGet, url.String(), nil, make(http.Header)) if err != nil { return false, fmt.Errorf("failed to create a Cryostat REST request: %s", err.Error()) } From b81422dfc2b4c172ca59b35b5fa00c440435eaf4 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Mon, 4 Mar 2024 18:05:52 -0800 Subject: [PATCH 39/39] chore(bundle): update scorecard image tags --- .../manifests/cryostat-operator.clusterserviceversion.yaml | 2 +- bundle/tests/scorecard/config.yaml | 6 +++--- config/scorecard/patches/custom.config.yaml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index b3209ff4..824ee8ed 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -54,7 +54,7 @@ metadata: capabilities: Seamless Upgrades categories: Monitoring, Developer Tools containerImage: quay.io/cryostat/cryostat-operator:2.5.0-dev - createdAt: "2024-02-12T19:49:13Z" + createdAt: "2024-03-05T02:05:10Z" description: JVM monitoring and profiling tool operatorframework.io/initialization-resource: |- { diff --git a/bundle/tests/scorecard/config.yaml b/bundle/tests/scorecard/config.yaml index cf894137..12b39361 100644 --- a/bundle/tests/scorecard/config.yaml +++ b/bundle/tests/scorecard/config.yaml @@ -70,7 +70,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - operator-install - image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502 + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20240305020416 labels: suite: cryostat test: operator-install @@ -80,7 +80,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502 + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20240305020416 labels: suite: cryostat test: cryostat-cr @@ -90,7 +90,7 @@ stages: - entrypoint: - cryostat-scorecard-tests - cryostat-recording - image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502 + image: quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20240305020416 labels: suite: cryostat test: cryostat-recording diff --git a/config/scorecard/patches/custom.config.yaml b/config/scorecard/patches/custom.config.yaml index cda5bfd2..527eac9a 100644 --- a/config/scorecard/patches/custom.config.yaml +++ b/config/scorecard/patches/custom.config.yaml @@ -8,7 +8,7 @@ entrypoint: - cryostat-scorecard-tests - operator-install - image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502" + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20240305020416" labels: suite: cryostat test: operator-install @@ -18,7 +18,7 @@ entrypoint: - cryostat-scorecard-tests - cryostat-cr - image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502" + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20240305020416" labels: suite: cryostat test: cryostat-cr @@ -28,7 +28,7 @@ entrypoint: - cryostat-scorecard-tests - cryostat-recording - image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20231214061502" + image: "quay.io/cryostat/cryostat-operator-scorecard:2.5.0-20240305020416" labels: suite: cryostat test: cryostat-recording