diff --git a/cmd/cluster/validatepullsecretext.go b/cmd/cluster/validatepullsecretext.go index d038ea314..fb62bf38c 100644 --- a/cmd/cluster/validatepullsecretext.go +++ b/cmd/cluster/validatepullsecretext.go @@ -31,6 +31,12 @@ var BackplaneClusterAdmin = "backplane-cluster-admin" // Default auth section used to validate ocm account email againstsa const cloudAuthKey = "cloud.openshift.com" +// Service log template URLs +const ( + ServiceLogMultipleSyncFailures = "https://raw.githubusercontent.com/openshift/managed-notifications/master/osd/pull_secret_multiple_sync_failures.json" + ServiceLogUpdatePullSecret = "https://raw.githubusercontent.com/openshift/managed-notifications/master/osd/update_pull_secret.json" //nolint:gosec // G101 false positive — URL, not a credential +) + type Result int // Const values for results table entries @@ -42,15 +48,17 @@ const ( // validatePullSecretExtOptions defines the struct for running validate-pull-secret command type validatePullSecretExtOptions struct { - account *v1.Account // Account which owns target cluster - clusterID string // Target cluster containing pull-secret to be validated against OCM values - reason string // Reason or justification for accessing sensitive data. (ie jira ticket) - ocm *sdk.Connection // openshift api client - results *tabwriter.Writer // Used for printing tabled results - log *logrus.Logger // Simple stderr logger - verboseLevel string // Logging level - useAccessToken bool // Flag to use OCM access token values for validations - useRegCreds bool // Flag to use OCM registry credentials values for validations + account *v1.Account // Account which owns target cluster + clusterID string // Target cluster containing pull-secret to be validated against OCM values + reason string // Reason or justification for accessing sensitive data. (ie jira ticket) + ocm *sdk.Connection // openshift api client + results *tabwriter.Writer // Used for printing tabled results + log *logrus.Logger // Simple stderr logger + verboseLevel string // Logging level + useAccessToken bool // Flag to use OCM access token values for validations + useRegCreds bool // Flag to use OCM registry credentials values for validations + skipServiceLogs bool // Flag to skip service logs + failuresByServiceLog map[string][]string // Track failures by template } const VPSExample string = ` @@ -59,6 +67,9 @@ const VPSExample string = ` # Exclude Access-Token, and Registry-Credential checks... osdctl cluster validate-pull-secret-ext --cluster-id ${CLUSTER_ID} --reason "OSD-XYZ" --skip-access-token --skip-registry-creds + + # Skip sending service logs (useful for testing) + osdctl cluster validate-pull-secret-ext --cluster-id ${CLUSTER_ID} --reason "OSD-XYZ" --skip-service-logs ` func newCmdValidatePullSecretExt() *cobra.Command { @@ -67,10 +78,14 @@ func newCmdValidatePullSecretExt() *cobra.Command { Use: "validate-pull-secret-ext --cluster-id $CLUSTER_ID", Short: "Extended checks to confirm pull-secret data is synced with current OCM data", Long: ` - Attempts to validate if a cluster's pull-secret auth values are in sync with the account's email, - registry_credential, and access token data stored in OCM. - If this is being executed against a cluster which is not owned by the current OCM account, - Region Lead permissions are required to view and validate the OCM AccessToken. + Attempts to validate if a cluster's pull-secret auth values are in sync with the account's email, + registry_credential, and access token data stored in OCM. + + Service logs are automatically sent for detected issues. Multiple failures are aggregated into + a single service log. Use --skip-service-logs to prevent sending service logs. + + If this is being executed against a cluster which is not owned by the current OCM account, + Region Lead permissions are required to view and validate the OCM AccessToken. `, Example: VPSExample, DisableAutoGenTag: true, @@ -84,6 +99,7 @@ func newCmdValidatePullSecretExt() *cobra.Command { validatePullSecretCmd.Flags().StringVarP(&ops.verboseLevel, "log-level", "l", "info", "debug, info, warn, error. (default=info)") validatePullSecretCmd.Flags().Bool("skip-registry-creds", false, "Exclude OCM Registry Credentials checks against cluster secret") validatePullSecretCmd.Flags().Bool("skip-access-token", false, "Exclude OCM AccessToken checks against cluster secret") + validatePullSecretCmd.Flags().BoolVar(&ops.skipServiceLogs, "skip-service-logs", false, "Skip sending service logs (useful for testing/automation)") _ = validatePullSecretCmd.MarkFlagRequired("reason") return validatePullSecretCmd @@ -145,6 +161,9 @@ func (o *validatePullSecretExtOptions) run() error { var regCreds []*v1.RegistryCredential = nil var accessToken *v1.AccessToken = nil + // Initialize failure tracking map + o.failuresByServiceLog = make(map[string][]string) + // Create OCM connection... o.ocm, err = utils.CreateConnection() if err != nil { @@ -180,6 +199,13 @@ func (o *validatePullSecretExtOptions) run() error { o.results.Flush() }() + // Defer sending aggregated service logs after all validations complete + defer func() { + if err := o.sendAggregatedServiceLogs(); err != nil { + o.log.Errorf("Failed to send aggregated service logs: %v", err) + } + }() + // get account info from OCM o.account, err = o.getOCMAccountInfo() if err != nil { @@ -194,7 +220,7 @@ func (o *validatePullSecretExtOptions) run() error { if err != nil { return err } - // This validation prompts user to send or not send a service log... + // Validate auth email err = o.validateAuthEmail(pullSecret, emailOCM, cloudAuthKey) if err != nil { fmt.Printf("Error validating pull-secret auth['%s] email.\nErr:'%s'\nWould you like to continue with validations? ", cloudAuthKey, err) @@ -263,23 +289,6 @@ func (o *validatePullSecretExtOptions) run() error { return nil } -func sendPullSecretServiceLog(clusterID string, err error) { - postCmd := servicelog.PostCmdOptions{ - Template: "https://raw.githubusercontent.com/openshift/managed-notifications/master/osd/pull_secret_change_breaking_upgradesync.json", - ClusterId: clusterID, - } - sendServiceLog(postCmd, fmt.Sprintf("%s", err)) -} - -func sendPullSecretMismatchServiceLog(clusterID string, err error) { - // Note: This will prompt the user to continue. - postCmd := servicelog.PostCmdOptions{ - Template: "https://raw.githubusercontent.com/openshift/managed-notifications/master/osd/pull_secret_user_mismatch.json", - ClusterId: clusterID, - } - sendServiceLog(postCmd, fmt.Sprintf("%s\n", err)) -} - func (o *validatePullSecretExtOptions) validateAuthEmail(pullSecret *corev1.Secret, emailOCM string, authKey string) error { // Extract email from cluster pull-secret. emailCluster, err := getPullSecretAuthEmail(pullSecret, authKey) @@ -290,14 +299,14 @@ func (o *validatePullSecretExtOptions) validateAuthEmail(pullSecret *corev1.Secr o.log.Errorf("Couldn't extract email address from pull secret for: '%s'"+ "This can mean the pull secret is misconfigured. Please verify the pull secret manually:\n"+ " oc get secret -n openshift-config pull-secret -o json | jq -r '.data[\".dockerconfigjson\"]' | base64 -d", errAENF.auth) - sendPullSecretServiceLog(o.clusterID, err) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, errAENF.auth) } if errors.Is(err, ErrSecretMissingDockerConfigJson) { - sendPullSecretServiceLog(o.clusterID, err) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, "pull-secret (missing .dockerconfigjson)") } var errSANF *ErrorSecretAuthNotFound if errors.As(err, &errSANF) { - sendPullSecretServiceLog(o.clusterID, err) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, errSANF.auth) } //Todo: Should this prompt for a service log for other errors too (such as fail to unmarshall)? return err @@ -309,7 +318,7 @@ func (o *validatePullSecretExtOptions) validateAuthEmail(pullSecret *corev1.Secr o.addResult("account.Email", authKey, pullSecret.Namespace, pullSecret.Name, "email", Fail) err = fmt.Errorf("pull-secret auth:'%s', email:'%s' doesn't match user email from OCM:'%s'", cloudAuthKey, emailCluster, emailOCM) o.log.Errorf("%s\n", err) - sendPullSecretMismatchServiceLog(o.clusterID, err) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, authKey) return err } o.addResult("account.Email", authKey, pullSecret.Namespace, pullSecret.Name, "email", Pass) @@ -344,6 +353,7 @@ func (o *validatePullSecretExtOptions) checkAccessTokenToPullSecret(accessToken if err != nil { o.addResult("access_token", akey, pullSecret.Namespace, pullSecret.Name, "auth", Fail) o.log.Errorf("OCM accessToken.auth['%s'], failed to fetch this auth from cluster pull-secret, err:'%s'", akey, err) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, akey) hasErrors = true // no matching auth present containing email + token continue @@ -353,6 +363,7 @@ func (o *validatePullSecretExtOptions) checkAccessTokenToPullSecret(accessToken // Record token mismatch o.addResult("access_token", akey, pullSecret.Namespace, pullSecret.Name, "token", Fail) o.log.Errorf("OCM accessToken.auth['%s'] does not match token found in cluster pull-secret ", akey) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, akey) hasErrors = true } else { o.addResult("access_token", akey, pullSecret.Namespace, pullSecret.Name, "token", Pass) @@ -363,6 +374,7 @@ func (o *validatePullSecretExtOptions) checkAccessTokenToPullSecret(accessToken // Record email mismatch o.addResult("access_token", akey, pullSecret.Namespace, pullSecret.Name, "email", Fail) o.log.Errorf("auth['%s'], pull-secret email:'%s' does not match OCM accessToken.email:'%s'", akey, psTokenAuth.Email(), auth.Email()) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, akey) hasErrors = true } else { o.addResult("access_token", akey, pullSecret.Namespace, pullSecret.Name, "email", Pass) @@ -391,21 +403,24 @@ func (o *validatePullSecretExtOptions) checkRegistryCredsAgainstPullSecret(regCr } o.log.Debugf("OCM registry_credential:'%s'\n", regCred.HREF()) // registry_credential.token is stored plain text in OCM, no need to decode here... - token := regCred.Token() - username := regCred.Username() - if len(token) <= 0 { + token, ok := regCred.GetToken() + if !ok { o.addResult("registry_credential", regCred.Registry().ID(), pullSecret.Namespace, pullSecret.Name, "token", Fail) o.log.Errorf("empty token for OCM registry_credential. See:'ocm get %s'", regCred.HREF()) setErr() } - if len(username) <= 0 { + + username, ok := regCred.GetUsername() + if !ok { o.addResult("registry_credential", regCred.Registry().ID(), pullSecret.Namespace, pullSecret.Name, "token", Fail) o.log.Errorf("empty username for registry_credential. See:'ocm get %s'", regCred.HREF()) setErr() } + if regErr { continue } + // Auth token in cluster's pull-secret data uses format: "username + ':' + token" regToken := fmt.Sprintf("%s:%s", username, token) //Get the exact registry name from the registry_credentials registry.id ... @@ -418,8 +433,8 @@ func (o *validatePullSecretExtOptions) checkRegistryCredsAgainstPullSecret(regCr continue } //The registry name is used to find the correct section in the cluster pull-secret data. - regName := registry.Name() - if len(regName) <= 0 { + regName, ok := registry.GetName() + if !ok { o.addResult("registry_credential", regCred.Registry().ID(), pullSecret.Namespace, pullSecret.Name, "registry", Fail) o.log.Errorf("empty name for registry_credential. See:'ocm get %s'", registry.HREF()) setErr() @@ -430,24 +445,34 @@ func (o *validatePullSecretExtOptions) checkRegistryCredsAgainstPullSecret(regCr if err != nil { o.addResult("registry_credential", regCred.Registry().ID(), pullSecret.Namespace, pullSecret.Name, "auth", Fail) o.log.Errorf("OCM registry_credential['%s'] failed to fetch auth section from cluster pull secret, err:'%s'", regName, err) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, regName) setErr() continue } // Check all auth sections matching registries found in the registry_credentials for matching emails... - secEmail := secTokenAuth.Email() + secEmail, ok := secTokenAuth.GetEmail() + if !ok { + o.addResult("account.Email", regCred.Registry().ID(), pullSecret.Namespace, pullSecret.Name, "email", Fail) + o.log.Errorf("empty email found in cluster pull-secret for auth section:'%s'", regName) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, regName) + setErr() + continue + } if emailOCM != secEmail { o.addResult("account.Email", regCred.Registry().ID(), pullSecret.Namespace, pullSecret.Name, "email", Fail) o.log.Errorf("pull-secret auth['%s'].email:'%s' does not match OCM account.Email:'%s'.", regName, secEmail, emailOCM) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, regName) setErr() // set the error but continue to check the token portion... } else { o.addResult("account.Email", regCred.Registry().ID(), pullSecret.Namespace, pullSecret.Name, "email", Pass) o.log.Debugf("OCM registry_credential['%s']. OCM and cluster emails match. PASS\n", regName) } // Get the token from the cluster pull_secret... - secToken := secTokenAuth.Auth() - if len(secToken) <= 0 { + secToken, ok := secTokenAuth.GetAuth() + if !ok { o.addResult("registry_credential", regCred.Registry().ID(), pullSecret.Namespace, pullSecret.Name, "token", Fail) o.log.Errorf("empty token found in cluster pull-secret for auth section:'%s', err:'%s'", regName, err) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, regName) setErr() continue } @@ -456,6 +481,7 @@ func (o *validatePullSecretExtOptions) checkRegistryCredsAgainstPullSecret(regCr if err != nil { o.addResult("registry_credential", regCred.Registry().ID(), pullSecret.Namespace, pullSecret.Name, "token", Fail) o.log.Errorf("error decoding token in cluster pull-secret for auth section:'%s', err:'%s'", regName, err) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, regName) setErr() continue } @@ -464,6 +490,7 @@ func (o *validatePullSecretExtOptions) checkRegistryCredsAgainstPullSecret(regCr o.addResult("registry_credential", regCred.Registry().ID(), pullSecret.Namespace, pullSecret.Name, "token", Fail) o.log.Errorf("OCM registry_credential['%s'] token did NOT match value found in cluster pull_secret!\n"+ "May need to sync ocm credentials to cluster pull secret.\n", regName) + o.recordServiceLogFailure(ServiceLogMultipleSyncFailures, regName) setErr() continue } else { @@ -638,7 +665,7 @@ func (o *validatePullSecretExtOptions) getOCMRegistryCredentials(accountID strin err := fmt.Errorf("registryCredentials not found for Account:'%s' in OCM", accountID) o.log.Errorf("%s\nSee: /api/accounts_mgmt/v1/registry_credentials -p search=\"account_id='%s'\"", err, accountID) postCmd := servicelog.PostCmdOptions{ - Template: "https://raw.githubusercontent.com/openshift/managed-notifications/master/osd/update_pull_secret.json", + Template: ServiceLogUpdatePullSecret, TemplateParams: []string{"REGISTRY=registry.redhat.io"}, ClusterId: o.clusterID, } @@ -674,6 +701,78 @@ func sendServiceLog(postCmd servicelog.PostCmdOptions, message string) error { return err } +// buildTemplateParameters creates the template parameter array for service log +func buildTemplateParameters(failures []string) []string { + failureList := strings.Join(failures, ", ") + return []string{fmt.Sprintf("FAILURE_LIST=%s", failureList)} +} + +// formatFailureDisplay formats the visual display of failures for user output +func formatFailureDisplay(category string, failures []string) string { + var output strings.Builder + fmt.Fprintf(&output, "\nPull Secret Validation Failures: %s\n", category) + fmt.Fprintf(&output, "Found %d failure(s):\n\n", len(failures)) + + for i, failure := range failures { + fmt.Fprintf(&output, " %d. %s\n", i+1, failure) + } + output.WriteString("\n") + + return output.String() +} + +// recordServiceLogFailure collects failures to be aggregated and sent at the end +func (o *validatePullSecretExtOptions) recordServiceLogFailure(template string, authSource string) { + if o.skipServiceLogs { + return // Don't collect if we're skipping service logs + } + + if o.failuresByServiceLog == nil { + o.failuresByServiceLog = make(map[string][]string) + } + + // Add authSource to the list for this template + o.failuresByServiceLog[template] = append(o.failuresByServiceLog[template], authSource) + o.log.Debugf("Recorded service log failure for template %s: %s", template, authSource) +} + +// sendAggregatedServiceLogs sends collected service logs after all validations complete +func (o *validatePullSecretExtOptions) sendAggregatedServiceLogs() error { + if o.skipServiceLogs { + o.log.Infof("Skipping service logs (--skip-service-logs flag set)") + return nil + } + + // Get all failures (we only use one template now) + allFailures := o.failuresByServiceLog[ServiceLogMultipleSyncFailures] + if len(allFailures) == 0 { + o.log.Infof("No validation failures requiring service logs") + return nil + } + + // Display failures to user + display := formatFailureDisplay("Pull Secret Issues", allFailures) + fmt.Print(display) + + // Build template parameters + templateParams := buildTemplateParameters(allFailures) + + // Use servicelog package's built-in prompting and validation + postCmd := servicelog.PostCmdOptions{ + Template: ServiceLogMultipleSyncFailures, + TemplateParams: templateParams, + ClusterId: o.clusterID, + } + + if err := postCmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error sending service log: %s\n", err) + return err + } + + o.log.Infof("Service log sent successfully") + return nil +} + // getPullSecretAuthEmail extract the email for a specific auth from the provided secret func getPullSecretAuthEmail(secret *corev1.Secret, authKey string) (string, error) { dockerConfigJsonBytes, found := secret.Data[".dockerconfigjson"] diff --git a/cmd/cluster/validatepullsecretext_test.go b/cmd/cluster/validatepullsecretext_test.go index 6e968c9ec..7b36d0f2e 100644 --- a/cmd/cluster/validatepullsecretext_test.go +++ b/cmd/cluster/validatepullsecretext_test.go @@ -2,11 +2,240 @@ package cluster import ( "reflect" + "strings" "testing" + "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" ) +// Test buildTemplateParameters - tests creating template parameter array +func Test_buildTemplateParameters(t *testing.T) { + tests := []struct { + name string + failures []string + expected []string + }{ + { + name: "single failure returns parameter", + failures: []string{"cloud.openshift.com"}, + expected: []string{"FAILURE_LIST=cloud.openshift.com"}, + }, + { + name: "multiple failures returns parameter list", + failures: []string{"cloud.openshift.com", "quay.io"}, + expected: []string{"FAILURE_LIST=cloud.openshift.com, quay.io"}, + }, + { + name: "three failures returns parameter list", + failures: []string{"cloud.openshift.com", "quay.io", "registry.redhat.io"}, + expected: []string{"FAILURE_LIST=cloud.openshift.com, quay.io, registry.redhat.io"}, + }, + { + name: "empty failures returns empty parameter", + failures: []string{}, + expected: []string{"FAILURE_LIST="}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := buildTemplateParameters(tt.failures) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("buildTemplateParameters() = %v, expected %v", result, tt.expected) + } + }) + } +} + +// Test formatFailureDisplay - tests formatting failure output +func Test_formatFailureDisplay(t *testing.T) { + tests := []struct { + name string + category string + failures []string + contains []string // Strings that should be in output + }{ + { + name: "single failure formatting", + category: "Pull Secret Issues", + failures: []string{"cloud.openshift.com"}, + contains: []string{ + "Pull Secret Issues", + "Found 1 failure(s)", + "1. cloud.openshift.com", + }, + }, + { + name: "multiple failures formatting", + category: "Pull Secret Issues", + failures: []string{"registry.redhat.io", "quay.io"}, + contains: []string{ + "Pull Secret Issues", + "Found 2 failure(s)", + "1. registry.redhat.io", + "2. quay.io", + }, + }, + { + name: "three failures formatting", + category: "Pull Secret Issues", + failures: []string{"cloud.openshift.com", "quay.io", "registry.redhat.io"}, + contains: []string{ + "Pull Secret Issues", + "Found 3 failure(s)", + "1. cloud.openshift.com", + "2. quay.io", + "3. registry.redhat.io", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := formatFailureDisplay(tt.category, tt.failures) + for _, expected := range tt.contains { + if !strings.Contains(result, expected) { + t.Errorf("formatFailureDisplay() missing expected string %q\nGot: %v", expected, result) + } + } + }) + } +} + +// Test recordServiceLogFailure - tests failure recording with skipServiceLogs flag +func Test_recordServiceLogFailure(t *testing.T) { + tests := []struct { + name string + skipServiceLogs bool + recordings []struct { + template string + authSource string + } + expectedCounts map[string]int + }{ + { + name: "records failure when not skipping", + skipServiceLogs: false, + recordings: []struct { + template string + authSource string + }{ + {"template1.json", "cloud.openshift.com"}, + }, + expectedCounts: map[string]int{"template1.json": 1}, + }, + { + name: "skips recording when flag set", + skipServiceLogs: true, + recordings: []struct { + template string + authSource string + }{ + {"template1.json", "cloud.openshift.com"}, + }, + expectedCounts: map[string]int{}, + }, + { + name: "records multiple failures for same template", + skipServiceLogs: false, + recordings: []struct { + template string + authSource string + }{ + {"template1.json", "cloud.openshift.com"}, + {"template1.json", "quay.io"}, + }, + expectedCounts: map[string]int{"template1.json": 2}, + }, + { + name: "records failures for different templates", + skipServiceLogs: false, + recordings: []struct { + template string + authSource string + }{ + {"template1.json", "cloud.openshift.com"}, + {"template2.json", "quay.io"}, + }, + expectedCounts: map[string]int{"template1.json": 1, "template2.json": 1}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := &validatePullSecretExtOptions{ + skipServiceLogs: tt.skipServiceLogs, + failuresByServiceLog: make(map[string][]string), + log: logrus.New(), + } + + // Record all failures + for _, rec := range tt.recordings { + opts.recordServiceLogFailure(rec.template, rec.authSource) + } + + // Verify counts for each template + for template, expectedCount := range tt.expectedCounts { + actualCount := len(opts.failuresByServiceLog[template]) + if actualCount != expectedCount { + t.Errorf("recordServiceLogFailure() recorded %d failures for %s, expected %d", + actualCount, template, expectedCount) + } + } + + // Verify no unexpected templates were recorded + if len(opts.failuresByServiceLog) != len(tt.expectedCounts) { + t.Errorf("recordServiceLogFailure() recorded %d templates, expected %d", + len(opts.failuresByServiceLog), len(tt.expectedCounts)) + } + }) + } +} + +func Test_recordServiceLogFailure_nilMap(t *testing.T) { + opts := &validatePullSecretExtOptions{ + skipServiceLogs: false, + failuresByServiceLog: nil, + log: logrus.New(), + } + + opts.recordServiceLogFailure("template.json", "cloud.openshift.com") + + if opts.failuresByServiceLog == nil { + t.Fatal("recordServiceLogFailure() did not initialize nil map") + } + if len(opts.failuresByServiceLog["template.json"]) != 1 { + t.Errorf("recordServiceLogFailure() expected 1 failure, got %d", len(opts.failuresByServiceLog["template.json"])) + } +} + +func Test_sendAggregatedServiceLogs_skipFlag(t *testing.T) { + opts := &validatePullSecretExtOptions{ + skipServiceLogs: true, + failuresByServiceLog: map[string][]string{"template.json": {"cloud.openshift.com"}}, + log: logrus.New(), + } + + err := opts.sendAggregatedServiceLogs() + if err != nil { + t.Errorf("sendAggregatedServiceLogs() with skip flag returned error: %v", err) + } +} + +func Test_sendAggregatedServiceLogs_noFailures(t *testing.T) { + opts := &validatePullSecretExtOptions{ + skipServiceLogs: false, + failuresByServiceLog: make(map[string][]string), + log: logrus.New(), + } + + err := opts.sendAggregatedServiceLogs() + if err != nil { + t.Errorf("sendAggregatedServiceLogs() with no failures returned error: %v", err) + } +} + func Test_getPullSecretAuthEmail(t *testing.T) { tests := []struct { name string diff --git a/docs/README.md b/docs/README.md index ca33c16f7..7006035e5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2497,10 +2497,14 @@ osdctl cluster validate-pull-secret --cluster-id [flags] ### osdctl cluster validate-pull-secret-ext - Attempts to validate if a cluster's pull-secret auth values are in sync with the account's email, - registry_credential, and access token data stored in OCM. - If this is being executed against a cluster which is not owned by the current OCM account, - Region Lead permissions are required to view and validate the OCM AccessToken. + Attempts to validate if a cluster's pull-secret auth values are in sync with the account's email, + registry_credential, and access token data stored in OCM. + + Service logs are automatically sent for detected issues. Multiple failures are aggregated into + a single service log. Use --skip-service-logs to prevent sending service logs. + + If this is being executed against a cluster which is not owned by the current OCM account, + Region Lead permissions are required to view and validate the OCM AccessToken. ``` @@ -2525,6 +2529,7 @@ osdctl cluster validate-pull-secret-ext --cluster-id $CLUSTER_ID [flags] --skip-access-token Exclude OCM AccessToken checks against cluster secret --skip-aws-proxy-check aws_proxy Don't use the configured aws_proxy value --skip-registry-creds Exclude OCM Registry Credentials checks against cluster secret + --skip-service-logs Skip sending service logs (useful for testing/automation) -S, --skip-version-check skip checking to see if this is the most recent release ``` diff --git a/docs/osdctl_cluster_validate-pull-secret-ext.md b/docs/osdctl_cluster_validate-pull-secret-ext.md index cfeca4b12..cbab7c547 100644 --- a/docs/osdctl_cluster_validate-pull-secret-ext.md +++ b/docs/osdctl_cluster_validate-pull-secret-ext.md @@ -5,10 +5,14 @@ Extended checks to confirm pull-secret data is synced with current OCM data ### Synopsis - Attempts to validate if a cluster's pull-secret auth values are in sync with the account's email, - registry_credential, and access token data stored in OCM. - If this is being executed against a cluster which is not owned by the current OCM account, - Region Lead permissions are required to view and validate the OCM AccessToken. + Attempts to validate if a cluster's pull-secret auth values are in sync with the account's email, + registry_credential, and access token data stored in OCM. + + Service logs are automatically sent for detected issues. Multiple failures are aggregated into + a single service log. Use --skip-service-logs to prevent sending service logs. + + If this is being executed against a cluster which is not owned by the current OCM account, + Region Lead permissions are required to view and validate the OCM AccessToken. ``` @@ -25,6 +29,9 @@ osdctl cluster validate-pull-secret-ext --cluster-id $CLUSTER_ID [flags] # Exclude Access-Token, and Registry-Credential checks... osdctl cluster validate-pull-secret-ext --cluster-id ${CLUSTER_ID} --reason "OSD-XYZ" --skip-access-token --skip-registry-creds + # Skip sending service logs (useful for testing) + osdctl cluster validate-pull-secret-ext --cluster-id ${CLUSTER_ID} --reason "OSD-XYZ" --skip-service-logs + ``` ### Options @@ -36,6 +43,7 @@ osdctl cluster validate-pull-secret-ext --cluster-id $CLUSTER_ID [flags] --reason string Mandatory reason for this command to be run (usually includes an OHSS or PD ticket) --skip-access-token Exclude OCM AccessToken checks against cluster secret --skip-registry-creds Exclude OCM Registry Credentials checks against cluster secret + --skip-service-logs Skip sending service logs (useful for testing/automation) ``` ### Options inherited from parent commands