diff --git a/cmd/certsuite/run/run.go b/cmd/certsuite/run/run.go index 5755a90a7..8ec37322f 100644 --- a/cmd/certsuite/run/run.go +++ b/cmd/certsuite/run/run.go @@ -45,6 +45,10 @@ func NewCommand() *cobra.Command { runCmd.PersistentFlags().String("daemonset-mem-req", "100M", "Memory request for the probe daemonset container") runCmd.PersistentFlags().String("daemonset-mem-lim", "100M", "Memory limit for the probe daemonset container") runCmd.PersistentFlags().Bool("sanitize-claim", false, "Sanitize the claim.json file before sending it to the collector") + runCmd.PersistentFlags().String("connect-api-key", "", "API Key for Red Hat Connect portal") + runCmd.PersistentFlags().String("connect-project-id", "", "Project ID for Red Hat Connect portal") + runCmd.PersistentFlags().String("connect-api-proxy-url", "", "Proxy URL for Red Hat Connect API") + runCmd.PersistentFlags().String("connect-api-proxy-port", "", "Proxy port for Red Hat Connect API") return runCmd } @@ -73,6 +77,10 @@ func initTestParamsFromFlags(cmd *cobra.Command) error { testParams.DaemonsetMemReq, _ = cmd.Flags().GetString("daemonset-mem-req") testParams.DaemonsetMemLim, _ = cmd.Flags().GetString("daemonset-mem-lim") testParams.SanitizeClaim, _ = cmd.Flags().GetBool("sanitize-claim") + testParams.ConnectAPIKey, _ = cmd.Flags().GetString("connect-api-key") + testParams.ConnectProjectID, _ = cmd.Flags().GetString("connect-project-id") + testParams.ConnectAPIProxyURL, _ = cmd.Flags().GetString("connect-api-proxy-url") + testParams.ConnectAPIProxyPort, _ = cmd.Flags().GetString("connect-api-proxy-port") timeoutStr, _ := cmd.Flags().GetString("timeout") // Check if the output directory exists and, if not, create it diff --git a/config/certsuite_config.yml b/config/certsuite_config.yml index 04f735c30..d48f91eaa 100644 --- a/config/certsuite_config.yml +++ b/config/certsuite_config.yml @@ -38,3 +38,7 @@ executedBy: "" partnerName: "" collectorAppPassword: "" collectorAppEndpoint: "http://claims-collector.cnf-certifications.sysdeseng.com" +connectAPIKey: "" +connectProjectID: "" +connectAPIProxyURL: "" +connectAPIProxyPort: "" diff --git a/internal/results/archiver.go b/internal/results/archiver.go index a1f1f8c38..76428e7cb 100644 --- a/internal/results/archiver.go +++ b/internal/results/archiver.go @@ -2,11 +2,17 @@ package results import ( "archive/tar" + "bytes" "compress/gzip" + "encoding/json" "fmt" "io" + "mime/multipart" + "net/http" + "net/url" "os" "path/filepath" + "strings" "time" "github.com/redhat-best-practices-for-k8s/certsuite/internal/log" @@ -16,6 +22,13 @@ const ( // tarGz file prefix layout format: YearMonthDay-HourMinSec tarGzFileNamePrefixLayout = "20060102-150405" tarGzFileNameSuffix = "cnf-test-results.tar.gz" + + // Connect API Information + certIDURL = "https://access.qa.redhat.com/hydra/cwe/rest/v1.0/projects/certifications" + connectAPIURL = "https://access.qa.redhat.com/hydra/cwe/rest/v1.0/attachments/upload" + + // certIDURL = "https://access.redhat.com/hydra/cwe/rest/v1.0/projects/certifications" + // connectAPIURL = "https://access.redhat.com/hydra/cwe/rest/v1.0/attachments/upload" ) func generateZipFileName() string { @@ -38,14 +51,14 @@ func getFileTarHeader(file string) (*tar.Header, error) { } // Creates a zip file in the outputDir containing each file in the filePaths slice. -func CompressResultsArtifacts(outputDir string, filePaths []string) error { +func CompressResultsArtifacts(outputDir string, filePaths []string) (string, error) { zipFileName := generateZipFileName() zipFilePath := filepath.Join(outputDir, zipFileName) log.Info("Compressing results artifacts into %s", zipFilePath) zipFile, err := os.Create(zipFilePath) if err != nil { - return fmt.Errorf("failed creating tar.gz file %s in dir %s (filepath=%s): %v", + return "", fmt.Errorf("failed creating tar.gz file %s in dir %s (filepath=%s): %v", zipFileName, outputDir, zipFilePath, err) } @@ -60,25 +73,226 @@ func CompressResultsArtifacts(outputDir string, filePaths []string) error { tarHeader, err := getFileTarHeader(file) if err != nil { - return err + return "", err } err = tarWriter.WriteHeader(tarHeader) if err != nil { - return fmt.Errorf("failed to write tar header for %s: %v", file, err) + return "", fmt.Errorf("failed to write tar header for %s: %v", file, err) } f, err := os.Open(file) if err != nil { - return fmt.Errorf("failed to open file %s: %v", file, err) + return "", fmt.Errorf("failed to open file %s: %v", file, err) } if _, err = io.Copy(tarWriter, f); err != nil { - return fmt.Errorf("failed to tar file %s: %v", file, err) + return "", fmt.Errorf("failed to tar file %s: %v", file, err) } f.Close() } + // Return the entire path to the zip file + return zipFilePath, nil +} + +func createFormField(w *multipart.Writer, field, value string) error { + fw, err := w.CreateFormField(field) + if err != nil { + return fmt.Errorf("failed to create form field: %v", err) + } + + _, err = fw.Write([]byte(value)) + if err != nil { + return fmt.Errorf("failed to write field %s: %v", field, err) + } + + return nil +} + +type CertIDResponse struct { + ID int `json:"id"` + CaseNumber string `json:"caseNumber"` + Status string `json:"status"` + CertificationLevel string `json:"certificationLevel"` + RhcertURL string `json:"rhcertUrl"` + HasStartedByPartner bool `json:"hasStartedByPartner"` + CertificationType struct { + ID int `json:"id"` + Name string `json:"name"` + } `json:"certificationType"` +} + +// GetCertIDFromConnectAPI gets the certification ID from the Red Hat Connect API +func GetCertIDFromConnectAPI(apiKey, projectID, proxyURL, proxyPort string) (string, error) { + log.Info("Getting certification ID from Red Hat Connect API") + + // sanitize the incoming variables, remove the double quotes if any + apiKey = strings.ReplaceAll(apiKey, "\"", "") + projectID = strings.ReplaceAll(projectID, "\"", "") + proxyURL = strings.ReplaceAll(proxyURL, "\"", "") + proxyPort = strings.ReplaceAll(proxyPort, "\"", "") + + // remove quotes from projectID + projectIDJSON := fmt.Sprintf(`{ "projectId": %q }`, projectID) + + // Convert JSON to bytes + projectIDJSONBytes := []byte(projectIDJSON) + + // Create a new request + req, err := http.NewRequest("POST", certIDURL, bytes.NewBuffer(projectIDJSONBytes)) + if err != nil { + return "", fmt.Errorf("failed to create new request: %v", err) + } + + log.Debug("Request Body: %s", req.Body) + + // Set the content type + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("x-api-key", apiKey) + + // print the request + log.Debug("Sending request to %s", certIDURL) + + client := &http.Client{} + setProxy(client, proxyURL, proxyPort) + res, err := sendRequest(req, client) + if err != nil { + return "", fmt.Errorf("failed to send post request to the endpoint: %v", err) + } + defer res.Body.Close() + + // Parse the response + var certIDResponse CertIDResponse + err = json.NewDecoder(res.Body).Decode(&certIDResponse) + if err != nil { + return "", fmt.Errorf("failed to decode response body: %v", err) + } + + // Return the certification ID + return fmt.Sprintf("%d", certIDResponse.ID), nil +} + +type UploadResult struct { + UUID string `json:"uuid"` + Type string `json:"type"` + Name string `json:"name"` + Size int `json:"size"` + ContentType string `json:"contentType"` + Desc string `json:"desc"` + DownloadURL string `json:"downloadUrl"` + UploadedBy string `json:"uploadedBy"` + UploadedDate time.Time `json:"uploadedDate"` + CertID int `json:"certId"` +} + +// SendResultsToConnectAPI sends the results to the Red Hat Connect API +// +//nolint:funlen +func SendResultsToConnectAPI(zipFile, apiKey, certID, proxyURL, proxyPort string) error { + log.Info("Sending results to Red Hat Connect") + + // sanitize the incoming variables, remove the double quotes if any + apiKey = strings.ReplaceAll(apiKey, "\"", "") + certID = strings.ReplaceAll(certID, "\"", "") + proxyURL = strings.ReplaceAll(proxyURL, "\"", "") + proxyPort = strings.ReplaceAll(proxyPort, "\"", "") + + var buffer bytes.Buffer + + // Create a new multipart writer + w := multipart.NewWriter(&buffer) + + fw, err := w.CreateFormFile("attachment", filepath.Base(zipFile)) + if err != nil { + return fmt.Errorf("failed to create form file: %v", err) + } + + _, err = fw.Write([]byte(zipFile)) + if err != nil { + return fmt.Errorf("failed to write file: %v", err) + } + + // Create a form field + err = createFormField(w, "type", "RhocpBestPracticeTestResult") + if err != nil { + return err + } + + // Create a form field + err = createFormField(w, "certId", certID) + if err != nil { + return err + } + + // Create a form field + err = createFormField(w, "description", "CNF Test Results") + if err != nil { + return err + } + + w.Close() + + // Create a new request + req, err := http.NewRequest("POST", connectAPIURL, &buffer) + if err != nil { + return fmt.Errorf("failed to create new request: %v", err) + } + + // Set the content type + req.Header.Set("Content-Type", w.FormDataContentType()) + req.Header.Set("x-api-key", apiKey) + + // Create a client + client := &http.Client{} + setProxy(client, proxyURL, proxyPort) + response, err := sendRequest(req, client) + if err != nil { + return fmt.Errorf("failed to send post request to the endpoint: %v", err) + } + defer response.Body.Close() + + // Parse the result of the request + var uploadResult UploadResult + err = json.NewDecoder(response.Body).Decode(&uploadResult) + if err != nil { + return fmt.Errorf("failed to decode response body: %v", err) + } + + log.Info("Download URL: %s", uploadResult.DownloadURL) + log.Info("Upload Date: %s", uploadResult.UploadedDate) return nil } + +func sendRequest(req *http.Request, client *http.Client) (*http.Response, error) { + // print the request + log.Debug("Sending request to %s", req.URL) + log.Info("request: %v", req) + + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send post request: %v", err) + } + + if res.StatusCode != http.StatusOK { + log.Debug("Response: %v", res) + return nil, fmt.Errorf("failed to send post request to the endpoint: %v", res.Status) + } + + return res, nil +} + +func setProxy(client *http.Client, proxyURL, proxyPort string) { + if proxyURL != "" && proxyPort != "" { + log.Debug("Proxy is set. Using proxy %s:%s", proxyURL, proxyPort) + proxyURL := fmt.Sprintf("%s:%s", proxyURL, proxyPort) + parsedURL, err := url.Parse(proxyURL) + if err != nil { + log.Error("Failed to parse proxy URL: %v", err) + } + log.Debug("Proxy URL: %s", parsedURL) + client.Transport = &http.Transport{Proxy: http.ProxyURL(parsedURL)} + } +} diff --git a/pkg/autodiscover/autodiscover.go b/pkg/autodiscover/autodiscover.go index 948020fa3..26d69eb5c 100644 --- a/pkg/autodiscover/autodiscover.go +++ b/pkg/autodiscover/autodiscover.go @@ -112,6 +112,10 @@ type DiscoveredTestData struct { PartnerName string CollectorAppPassword string CollectorAppEndpoint string + ConnectAPIKey string + ConnectProjectID string + ConnectAPIProxyURL string + ConnectAPIProxyPort string } type labelObject struct { @@ -322,6 +326,10 @@ func DoAutoDiscover(config *configuration.TestConfiguration) DiscoveredTestData data.PartnerName = config.PartnerName data.CollectorAppPassword = config.CollectorAppPassword data.CollectorAppEndpoint = config.CollectorAppEndpoint + data.ConnectAPIKey = config.ConnectAPIKey + data.ConnectProjectID = config.ConnectProjectID + data.ConnectAPIProxyURL = config.ConnectAPIProxyURL + data.ConnectAPIProxyPort = config.ConnectAPIProxyPort return data } diff --git a/pkg/certsuite/certsuite.go b/pkg/certsuite/certsuite.go index 1b9245407..5c7b5939b 100644 --- a/pkg/certsuite/certsuite.go +++ b/pkg/certsuite/certsuite.go @@ -128,7 +128,7 @@ func Shutdown() { } } -//nolint:funlen +//nolint:funlen,gocyclo func Run(labelsFilter, outputFolder string) error { testParams := configuration.GetTestParameters() @@ -201,12 +201,47 @@ func Run(labelsFilter, outputFolder string) error { // Add the log file path allArtifactsFilePaths = append(allArtifactsFilePaths, filepath.Join(outputFolder, log.LogFileName)) + // Red Hat Connect API key and project ID are required to send the tar.gz to Red Hat Connect. + sendToConnectAPI := false + if env.ConnectAPIKey != "" && env.ConnectProjectID != "" { + log.Info("Sending results to Red Hat Connect API with API key %s and project ID %s", env.ConnectAPIKey, env.ConnectProjectID) + sendToConnectAPI = true + } else { + log.Info("Red Hat Connect API key and project ID are not set. Results will not be sent to Red Hat Connect.") + } + // tar.gz file creation with results and html artifacts, unless omitted by env var. - if !configuration.GetTestParameters().OmitArtifactsZipFile { - err = results.CompressResultsArtifacts(resultsOutputDir, allArtifactsFilePaths) + if !configuration.GetTestParameters().OmitArtifactsZipFile || sendToConnectAPI { + zipFile, err := results.CompressResultsArtifacts(resultsOutputDir, allArtifactsFilePaths) if err != nil { log.Fatal("Failed to compress results artifacts: %v", err) } + + if sendToConnectAPI { + log.Debug("Get CertificationID from the Red Hat Connect API") + certificationID, err := results.GetCertIDFromConnectAPI( + env.ConnectAPIKey, env.ConnectProjectID, env.ConnectAPIProxyURL, env.ConnectAPIProxyPort) + if err != nil { + log.Fatal("Failed to get CertificationID from Red Hat Connect: %v", err) + } + + log.Debug("Sending ZIP file %s to Red Hat Connect", zipFile) + err = results.SendResultsToConnectAPI(zipFile, + env.ConnectAPIKey, certificationID, env.ConnectAPIProxyURL, env.ConnectAPIProxyPort) + if err != nil { + log.Fatal("Failed to send results to Red Hat Connect: %v", err) + } + + log.Info("Results successfully sent to Red Hat Connect with CertificationID %s", certificationID) + } + + if !configuration.GetTestParameters().OmitArtifactsZipFile { + // delete the zip as the user does not want it. + err = os.Remove(zipFile) + if err != nil { + log.Fatal("Failed to remove zip file %s: %v", zipFile, err) + } + } } // Remove web artifacts if user does not want them. diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go index ee449c003..f27382cb1 100644 --- a/pkg/configuration/configuration.go +++ b/pkg/configuration/configuration.go @@ -92,6 +92,10 @@ type TestConfiguration struct { PartnerName string `yaml:"partnerName,omitempty" json:"partnerName,omitempty"` CollectorAppPassword string `yaml:"collectorAppPassword,omitempty" json:"collectorAppPassword,omitempty"` CollectorAppEndpoint string `yaml:"collectorAppEndpoint,omitempty" json:"collectorAppEndpoint,omitempty"` + ConnectAPIKey string `yaml:"connectAPIKey,omitempty" json:"connectAPIKey,omitempty"` + ConnectProjectID string `yaml:"connectProjectID,omitempty" json:"connectProjectID,omitempty"` + ConnectAPIProxyURL string `yaml:"connectAPIProxyURL,omitempty" json:"connectAPIProxyURL,omitempty"` + ConnectAPIProxyPort string `yaml:"connectAPIProxyPort,omitempty" json:"connectAPIProxyPort,omitempty"` } type TestParameters struct { @@ -117,4 +121,8 @@ type TestParameters struct { EnableXMLCreation bool ServerMode bool Timeout time.Duration + ConnectAPIKey string + ConnectProjectID string + ConnectAPIProxyURL string + ConnectAPIProxyPort string } diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index c01d57975..ab9a8b473 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -135,6 +135,10 @@ type TestEnvironment struct { // rename this with testTarget PartnerName string CollectorAppPassword string CollectorAppEndpoint string + ConnectAPIKey string + ConnectProjectID string + ConnectAPIProxyURL string + ConnectAPIProxyPort string SkipPreflight bool } @@ -366,6 +370,10 @@ func buildTestEnvironment() { //nolint:funlen,gocyclo env.PartnerName = data.PartnerName env.CollectorAppPassword = data.CollectorAppPassword env.CollectorAppEndpoint = data.CollectorAppEndpoint + env.ConnectAPIKey = data.ConnectAPIKey + env.ConnectProjectID = data.ConnectProjectID + env.ConnectAPIProxyURL = data.ConnectAPIProxyURL + env.ConnectAPIProxyPort = data.ConnectAPIProxyPort operators := createOperators(data.Csvs, data.AllSubscriptions, data.AllPackageManifests, data.AllInstallPlans, data.AllCatalogSources, false, true)