-
Notifications
You must be signed in to change notification settings - Fork 138
ROSAENG-2066: Add osdctl account aws-creds command #913
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,241 @@ | ||||||||||||||||||
| package account | ||||||||||||||||||
|
|
||||||||||||||||||
| import ( | ||||||||||||||||||
| "context" | ||||||||||||||||||
| "fmt" | ||||||||||||||||||
| "io" | ||||||||||||||||||
|
|
||||||||||||||||||
| "github.com/fatih/color" | ||||||||||||||||||
| "github.com/openshift/osdctl/pkg/controller" | ||||||||||||||||||
| "github.com/openshift/osdctl/pkg/utils" | ||||||||||||||||||
| "github.com/spf13/cobra" | ||||||||||||||||||
| "k8s.io/cli-runtime/pkg/genericclioptions" | ||||||||||||||||||
| cmdutil "k8s.io/kubectl/pkg/cmd/util" | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| type awsCredsRotateOptions struct { | ||||||||||||||||||
| awsCredsOptions | ||||||||||||||||||
| rotateManagedAdmin bool | ||||||||||||||||||
| rotateCcsAdmin bool | ||||||||||||||||||
| refreshSecrets bool | ||||||||||||||||||
| dryRun bool | ||||||||||||||||||
| force bool | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // newCmdAWSCredsRotate creates the "aws-creds rotate" subcommand for IAM credential rotation. | ||||||||||||||||||
| func newCmdAWSCredsRotate(streams genericclioptions.IOStreams) *cobra.Command { | ||||||||||||||||||
| ops := &awsCredsRotateOptions{ | ||||||||||||||||||
| awsCredsOptions: awsCredsOptions{IOStreams: streams, log: newAWSCredsLogger()}, | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| cmd := &cobra.Command{ | ||||||||||||||||||
| Use: "rotate -C <cluster-id> --reason <reason> [flags]", | ||||||||||||||||||
| Short: "Rotate AWS IAM credentials for a cluster", | ||||||||||||||||||
| Long: `Rotates AWS IAM credentials for osdManagedAdmin and/or osdCcsAdmin users. | ||||||||||||||||||
| Runs a diagnostic snapshot first, then performs the rotation with | ||||||||||||||||||
| interactive confirmation. | ||||||||||||||||||
|
|
||||||||||||||||||
| Use --refresh-secrets to only delete and recreate CredentialRequest secrets | ||||||||||||||||||
| without rotating AWS keys or modifying Hive secrets. This is useful when | ||||||||||||||||||
| CCO needs to re-provision secrets with existing credentials. | ||||||||||||||||||
|
|
||||||||||||||||||
| AWS credentials are obtained via backplane by default, falling back to the | ||||||||||||||||||
| default AWS credential chain (env vars, ~/.aws/config). Use --aws-profile | ||||||||||||||||||
| to specify a named profile, or --aws-use-env to skip backplane and use | ||||||||||||||||||
| environment credentials directly (e.g. after rh-aws-saml-login). | ||||||||||||||||||
|
|
||||||||||||||||||
| Pre-flight checks (IAM permissions, secret existence) block rotation by | ||||||||||||||||||
| default. Use --force to allow proceeding past errors with explicit YES | ||||||||||||||||||
| confirmation — only when you are certain the errors are benign.`, | ||||||||||||||||||
| Example: ` # Rotate osdManagedAdmin credentials | ||||||||||||||||||
| osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --managed-admin | ||||||||||||||||||
|
|
||||||||||||||||||
| # Rotate osdCcsAdmin credentials (CCS clusters only) | ||||||||||||||||||
| osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --ccs-admin | ||||||||||||||||||
|
|
||||||||||||||||||
| # Rotate both | ||||||||||||||||||
| osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --managed-admin --ccs-admin | ||||||||||||||||||
|
|
||||||||||||||||||
| # Only refresh CredentialRequest secrets (no key rotation) | ||||||||||||||||||
| osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --refresh-secrets | ||||||||||||||||||
|
|
||||||||||||||||||
| # Dry-run: preview what would happen | ||||||||||||||||||
| osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --managed-admin --dry-run | ||||||||||||||||||
|
|
||||||||||||||||||
| # Using rh-aws-saml-login credentials (no backplane) | ||||||||||||||||||
| kinit $USER@IPA.REDHAT.COM | ||||||||||||||||||
| eval $(rh-aws-saml-login --output env rhcontrol) | ||||||||||||||||||
| export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN | ||||||||||||||||||
| osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --managed-admin --aws-use-env | ||||||||||||||||||
|
|
||||||||||||||||||
| # With staging cluster and production hive | ||||||||||||||||||
| osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --managed-admin --hive-ocm-url production`, | ||||||||||||||||||
| DisableAutoGenTag: true, | ||||||||||||||||||
| Run: func(cmd *cobra.Command, args []string) { | ||||||||||||||||||
| cmdutil.CheckErr(ops.validateRotate(cmd, args)) | ||||||||||||||||||
| cmdutil.CheckErr(runRotate(cmd.Context(), ops)) | ||||||||||||||||||
| }, | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| ops.addFlags(cmd) | ||||||||||||||||||
| cmd.Flags().BoolVar(&ops.rotateManagedAdmin, "managed-admin", false, "Rotate osdManagedAdmin credentials") | ||||||||||||||||||
| cmd.Flags().BoolVar(&ops.rotateCcsAdmin, "ccs-admin", false, "Rotate osdCcsAdmin credentials (CCS clusters only)") | ||||||||||||||||||
| cmd.Flags().BoolVar(&ops.refreshSecrets, "refresh-secrets", false, "Only delete and recreate CredentialRequest secrets (no key rotation)") | ||||||||||||||||||
| cmd.Flags().BoolVar(&ops.dryRun, "dry-run", false, "Preview rotation actions without making changes") | ||||||||||||||||||
| cmd.Flags().BoolVar(&ops.force, "force", false, "Allow proceeding past pre-flight errors with YES confirmation. Use only when certain the errors are benign (e.g., known SCP restrictions that won't affect rotation)") | ||||||||||||||||||
|
|
||||||||||||||||||
| return cmd | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // validateRotate validates that exactly one rotation mode is selected and flags are not conflicting. | ||||||||||||||||||
| func (o *awsCredsRotateOptions) validateRotate(cmd *cobra.Command, args []string) error { | ||||||||||||||||||
| if err := o.validate(cmd, args); err != nil { | ||||||||||||||||||
| return err | ||||||||||||||||||
| } | ||||||||||||||||||
| if o.refreshSecrets && (o.rotateManagedAdmin || o.rotateCcsAdmin) { | ||||||||||||||||||
| return cmdutil.UsageErrorf(cmd, "--refresh-secrets cannot be combined with --managed-admin or --ccs-admin") | ||||||||||||||||||
| } | ||||||||||||||||||
| if !o.rotateManagedAdmin && !o.rotateCcsAdmin && !o.refreshSecrets { | ||||||||||||||||||
| return cmdutil.UsageErrorf(cmd, "at least one of --managed-admin, --ccs-admin, or --refresh-secrets is required") | ||||||||||||||||||
| } | ||||||||||||||||||
| return nil | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // confirmStrictYES requires the user to type "YES" exactly to proceed. | ||||||||||||||||||
| func confirmStrictYES(in io.Reader, out io.Writer) bool { | ||||||||||||||||||
| fmt.Fprintf(out, "Type YES to continue: ") | ||||||||||||||||||
| var response string | ||||||||||||||||||
| if _, err := fmt.Fscanln(in, &response); err != nil { | ||||||||||||||||||
| return false | ||||||||||||||||||
| } | ||||||||||||||||||
| return response == "YES" | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // runRotate orchestrates the full rotation workflow: cluster identification, | ||||||||||||||||||
| // diagnostic snapshot, permission verification, and credential rotation. | ||||||||||||||||||
| func runRotate(ctx context.Context, o *awsCredsRotateOptions) error { | ||||||||||||||||||
|
|
||||||||||||||||||
| rc, err := o.identifyCluster() | ||||||||||||||||||
| if err != nil { | ||||||||||||||||||
| return err | ||||||||||||||||||
| } | ||||||||||||||||||
| defer rc.ocmConn.Close() | ||||||||||||||||||
|
|
||||||||||||||||||
| if o.rotateCcsAdmin && !rc.isCCS { | ||||||||||||||||||
| return fmt.Errorf("--ccs-admin specified but cluster is not CCS/BYOC") | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if o.refreshSecrets { | ||||||||||||||||||
| if err := o.resolveForCRSecrets(ctx, rc); err != nil { | ||||||||||||||||||
| return err | ||||||||||||||||||
| } | ||||||||||||||||||
| report, err := controller.DiagnoseCRSecrets(ctx, rc.hiveClient, rc.managedClient, rc.claimName, rc.account, o.Out) | ||||||||||||||||||
| if err != nil { | ||||||||||||||||||
| return err | ||||||||||||||||||
| } | ||||||||||||||||||
| controller.RenderCredRequestTable(report, o.Out) | ||||||||||||||||||
| return runRefreshSecrets(ctx, o, rc, report) | ||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if err := o.resolveCluster(ctx, rc); err != nil { | ||||||||||||||||||
| return err | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| o.log.Info("Running pre-rotation diagnostic snapshot") | ||||||||||||||||||
| input := rc.toCredsInput(o.log, o.Out) | ||||||||||||||||||
| report, err := controller.DiagnoseCredentials(ctx, input) | ||||||||||||||||||
| if err != nil { | ||||||||||||||||||
| return err | ||||||||||||||||||
| } | ||||||||||||||||||
| controller.RenderReport(report, o.Out) | ||||||||||||||||||
|
|
||||||||||||||||||
| if !report.AllPermissionsOK { | ||||||||||||||||||
| o.log.Warn("IAM permission check detected issues") | ||||||||||||||||||
| red := color.New(color.FgRed).SprintFunc() | ||||||||||||||||||
| fmt.Fprintf(o.Out, "\n%s Pre-flight permission checks detected issues.\n", red("[FAIL]")) | ||||||||||||||||||
| fmt.Fprintln(o.Out, "Proceeding may result in failures during rotation or CR secret recreation.") | ||||||||||||||||||
| if !o.force { | ||||||||||||||||||
| fmt.Fprintln(o.Out, "Use --force to allow proceeding with YES confirmation.") | ||||||||||||||||||
| return fmt.Errorf("pre-flight permission checks failed — use --force to override") | ||||||||||||||||||
| } | ||||||||||||||||||
| fmt.Fprintln(o.Out, "--force specified. Type YES to confirm you want to proceed despite errors.") | ||||||||||||||||||
| if !confirmStrictYES(o.In, o.Out) { | ||||||||||||||||||
| o.log.Info("Operation cancelled by user") | ||||||||||||||||||
| return nil | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if o.dryRun { | ||||||||||||||||||
| o.log.Info("Dry-run mode — no changes will be made") | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| rotateInput := &controller.RotateSecretInput{ | ||||||||||||||||||
| AccountCRName: rc.claimName, | ||||||||||||||||||
| Account: rc.account, | ||||||||||||||||||
| OsdManagedAdminUsername: rc.adminUsername, | ||||||||||||||||||
| UpdateManagedAdminCreds: o.rotateManagedAdmin, | ||||||||||||||||||
| UpdateCcsCreds: o.rotateCcsAdmin, | ||||||||||||||||||
| DryRun: o.dryRun, | ||||||||||||||||||
| SkipPermissionCheck: !report.AllPermissionsOK, | ||||||||||||||||||
| AwsClient: rc.awsClient, | ||||||||||||||||||
| HiveKubeClient: rc.hiveClient, | ||||||||||||||||||
| ManagedClusterClient: rc.managedClient, | ||||||||||||||||||
| Report: report, | ||||||||||||||||||
| Log: o.log, | ||||||||||||||||||
| In: o.In, | ||||||||||||||||||
| Out: o.Out, | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if !o.dryRun { | ||||||||||||||||||
| o.log.Warn("Credential rotation will modify IAM keys and Hive secrets") | ||||||||||||||||||
| fmt.Fprintln(o.Out, "\nProceed with credential rotation?") | ||||||||||||||||||
| if !utils.ConfirmPrompt() { | ||||||||||||||||||
| o.log.Info("Rotation cancelled by user") | ||||||||||||||||||
| return nil | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| o.log.Info("Starting credential rotation") | ||||||||||||||||||
| if err := controller.RotateSecret(ctx, rotateInput); err != nil { | ||||||||||||||||||
| o.log.WithError(err).Error("Credential rotation failed") | ||||||||||||||||||
| return err | ||||||||||||||||||
| } | ||||||||||||||||||
| o.log.Info("Credential rotation completed successfully") | ||||||||||||||||||
| return nil | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // runRefreshSecrets deletes and recreates CredentialRequest secrets without rotating AWS keys. | ||||||||||||||||||
| func runRefreshSecrets(ctx context.Context, o *awsCredsRotateOptions, rc *resolvedCluster, report *controller.DiagnosticReport) error { | ||||||||||||||||||
|
|
||||||||||||||||||
| if rc.managedClient == nil { | ||||||||||||||||||
| return fmt.Errorf("managed cluster client not available — cannot refresh secrets") | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if report.ClusterRootKeyID == "" { | ||||||||||||||||||
| return fmt.Errorf("cannot refresh CredentialRequest secrets: kube-system/aws-creds is missing or unreadable on the managed cluster — CCO needs this secret to recreate operator credentials") | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if o.dryRun { | ||||||||||||||||||
| o.log.Info("Dry-run mode — no secrets will be deleted") | ||||||||||||||||||
| fmt.Fprintln(o.Out, "\n[Dry Run] Would delete and recreate all CredentialRequest secrets.") | ||||||||||||||||||
| fmt.Fprintln(o.Out, "[Dry Run] No AWS keys or Hive secrets would be modified.") | ||||||||||||||||||
| return nil | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| fmt.Fprintf(o.Out, "\nThis will delete %d CredentialRequest secret(s) so CCO recreates them.\n", len(report.CredRequests)) | ||||||||||||||||||
| fmt.Fprintln(o.Out, "No AWS keys or Hive secrets will be modified.") | ||||||||||||||||||
| fmt.Fprintln(o.Out, "\nProceed with secret refresh?") | ||||||||||||||||||
| if !utils.ConfirmPrompt() { | ||||||||||||||||||
| o.log.Info("Refresh cancelled by user") | ||||||||||||||||||
| return nil | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+228
to
+231
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Should we really give this option at all instead of stopping immediately? Any issue will likely result in one or multiple operators on the cluster breaking after the rotation - maybe we should instead call out opening a proactive case / responding in-case with the problems first?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Addressed — permission failures now hard-stop rotation by default. |
||||||||||||||||||
|
|
||||||||||||||||||
| o.log.Info("Deleting credential secrets for CCO to recreate") | ||||||||||||||||||
| if err := controller.DeleteCredentialSecrets(ctx, rc.managedClient, o.In, o.Out); err != nil { | ||||||||||||||||||
| o.log.WithError(err).Error("Failed to refresh credential secrets") | ||||||||||||||||||
| return err | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| o.log.Info("Credential secret refresh completed successfully") | ||||||||||||||||||
| return nil | ||||||||||||||||||
| } | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package account | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/openshift/osdctl/pkg/controller" | ||
| "github.com/spf13/cobra" | ||
| "k8s.io/cli-runtime/pkg/genericclioptions" | ||
| cmdutil "k8s.io/kubectl/pkg/cmd/util" | ||
| ) | ||
|
|
||
| type awsCredsSnapshotOptions struct { | ||
| awsCredsOptions | ||
| crSecretsOnly bool | ||
| } | ||
|
|
||
| // newCmdAWSCredsSnapshot creates the "aws-creds snapshot" subcommand for read-only credential diagnostics. | ||
| func newCmdAWSCredsSnapshot(streams genericclioptions.IOStreams) *cobra.Command { | ||
| ops := &awsCredsSnapshotOptions{ | ||
| awsCredsOptions: awsCredsOptions{IOStreams: streams, log: newAWSCredsLogger()}, | ||
| } | ||
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "snapshot -C <cluster-id> --reason <reason> [flags]", | ||
| Short: "Show a read-only credential status report for a cluster", | ||
| Long: `Produces a diagnostic report of AWS IAM credentials including: | ||
| - IAM access keys and which Hive secrets reference them | ||
| - CredentialRequest secrets and whether they need refresh | ||
| - IAM permission simulation (SCP/policy restriction detection) | ||
|
|
||
| Use --cr-secrets to show only the CredentialRequest secrets table. | ||
|
|
||
| This is a read-only operation — no credentials are modified. | ||
|
|
||
| AWS credentials are obtained via backplane by default, falling back to the | ||
| default AWS credential chain (env vars, ~/.aws/config). Use --aws-profile | ||
| to specify a named profile, or --aws-use-env to skip backplane and use | ||
| environment credentials directly (e.g. after rh-aws-saml-login).`, | ||
| Example: ` # Full credential status report (uses backplane) | ||
| osdctl account aws-creds snapshot -C $CLUSTER_ID --reason "$JIRA_TICKET" | ||
|
|
||
| # Only show CredentialRequest secret status | ||
| osdctl account aws-creds snapshot -C $CLUSTER_ID --reason "$JIRA_TICKET" --cr-secrets | ||
|
|
||
| # Using rh-aws-saml-login credentials (no backplane) | ||
| kinit $USER@IPA.REDHAT.COM | ||
| eval $(rh-aws-saml-login --output env rhcontrol) | ||
| export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN | ||
| osdctl account aws-creds snapshot -C $CLUSTER_ID --reason "$JIRA_TICKET" --aws-use-env | ||
|
|
||
| # With staging cluster and production hive | ||
| osdctl account aws-creds snapshot -C $CLUSTER_ID --reason "$JIRA_TICKET" --hive-ocm-url production`, | ||
| DisableAutoGenTag: true, | ||
| Run: func(cmd *cobra.Command, args []string) { | ||
| cmdutil.CheckErr(ops.validate(cmd, args)) | ||
| cmdutil.CheckErr(runSnapshot(cmd.Context(), ops)) | ||
| }, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| ops.addFlags(cmd) | ||
| cmd.Flags().BoolVar(&ops.crSecretsOnly, "cr-secrets", false, "Only show CredentialRequest secrets status") | ||
| return cmd | ||
| } | ||
|
|
||
| // runSnapshot produces the diagnostic report, either full or CR-secrets-only based on flags. | ||
| func runSnapshot(ctx context.Context, o *awsCredsSnapshotOptions) error { | ||
|
|
||
| rc, err := o.identifyCluster() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer rc.ocmConn.Close() | ||
|
|
||
| if o.crSecretsOnly { | ||
| if err := o.resolveForCRSecrets(ctx, rc); err != nil { | ||
| return err | ||
| } | ||
| report, err := controller.DiagnoseCRSecrets(ctx, rc.hiveClient, rc.managedClient, rc.claimName, rc.account, o.Out) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| controller.RenderCredRequestTable(report, o.Out) | ||
| return nil | ||
| } | ||
|
|
||
| if err := o.resolveCluster(ctx, rc); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| input := rc.toCredsInput(o.log, o.Out) | ||
| report, err := controller.DiagnoseCredentials(ctx, input) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| controller.RenderReport(report, o.Out) | ||
| return nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.