Skip to content

Commit 3ec12df

Browse files
nephomaniacclaude
andcommitted
ROSAENG-2066: Add osdctl account aws-creds command
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5e39b39 commit 3ec12df

15 files changed

Lines changed: 4797 additions & 253 deletions

cmd/account/aws-creds-rotate.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package account
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
8+
"github.com/fatih/color"
9+
"github.com/openshift/osdctl/pkg/controller"
10+
"github.com/openshift/osdctl/pkg/utils"
11+
"github.com/spf13/cobra"
12+
"k8s.io/cli-runtime/pkg/genericclioptions"
13+
cmdutil "k8s.io/kubectl/pkg/cmd/util"
14+
)
15+
16+
type awsCredsRotateOptions struct {
17+
awsCredsOptions
18+
rotateManagedAdmin bool
19+
rotateCcsAdmin bool
20+
refreshSecrets bool
21+
dryRun bool
22+
}
23+
24+
func newCmdAWSCredsRotate(streams genericclioptions.IOStreams) *cobra.Command {
25+
ops := &awsCredsRotateOptions{
26+
awsCredsOptions: awsCredsOptions{IOStreams: streams, log: newAWSCredsLogger()},
27+
}
28+
29+
cmd := &cobra.Command{
30+
Use: "rotate -C <cluster-id> --reason <reason> [flags]",
31+
Short: "Rotate AWS IAM credentials for a cluster",
32+
Long: `Rotates AWS IAM credentials for osdManagedAdmin and/or osdCcsAdmin users.
33+
Runs a diagnostic snapshot first, then performs the rotation with
34+
interactive confirmation.
35+
36+
Use --refresh-secrets to only delete and recreate CredentialRequest secrets
37+
without rotating AWS keys or modifying Hive secrets. This is useful when
38+
CCO needs to re-provision secrets with existing credentials.
39+
40+
AWS credentials are obtained via backplane by default. Use --aws-profile
41+
to override with a local AWS profile and manual role chaining.`,
42+
Example: ` # Rotate osdManagedAdmin credentials
43+
osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --managed-admin
44+
45+
# Rotate osdCcsAdmin credentials (CCS clusters only)
46+
osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --ccs-admin
47+
48+
# Rotate both
49+
osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --managed-admin --ccs-admin
50+
51+
# Only refresh CredentialRequest secrets (no key rotation)
52+
osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --refresh-secrets
53+
54+
# Dry-run: preview what would happen
55+
osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --managed-admin --dry-run
56+
57+
# With staging cluster and production hive
58+
osdctl account aws-creds rotate -C $CLUSTER_ID --reason "$JIRA_TICKET" --managed-admin --hive-ocm-url production`,
59+
DisableAutoGenTag: true,
60+
Run: func(cmd *cobra.Command, args []string) {
61+
cmdutil.CheckErr(ops.validateRotate(cmd, args))
62+
cmdutil.CheckErr(runRotate(ops))
63+
},
64+
}
65+
66+
ops.addFlags(cmd)
67+
cmd.Flags().BoolVar(&ops.rotateManagedAdmin, "managed-admin", false, "Rotate osdManagedAdmin credentials")
68+
cmd.Flags().BoolVar(&ops.rotateCcsAdmin, "ccs-admin", false, "Rotate osdCcsAdmin credentials (CCS clusters only)")
69+
cmd.Flags().BoolVar(&ops.refreshSecrets, "refresh-secrets", false, "Only delete and recreate CredentialRequest secrets (no key rotation)")
70+
cmd.Flags().BoolVar(&ops.dryRun, "dry-run", false, "Preview rotation actions without making changes")
71+
72+
return cmd
73+
}
74+
75+
func (o *awsCredsRotateOptions) validateRotate(cmd *cobra.Command, args []string) error {
76+
if err := o.validate(cmd, args); err != nil {
77+
return err
78+
}
79+
if o.refreshSecrets && (o.rotateManagedAdmin || o.rotateCcsAdmin) {
80+
return cmdutil.UsageErrorf(cmd, "--refresh-secrets cannot be combined with --managed-admin or --ccs-admin")
81+
}
82+
if !o.rotateManagedAdmin && !o.rotateCcsAdmin && !o.refreshSecrets {
83+
return cmdutil.UsageErrorf(cmd, "at least one of --managed-admin, --ccs-admin, or --refresh-secrets is required")
84+
}
85+
return nil
86+
}
87+
88+
// confirmStrictYES requires the user to type "YES" exactly to proceed.
89+
func confirmStrictYES(in io.Reader, out io.Writer) bool {
90+
fmt.Fprintf(out, "Type YES to continue: ")
91+
var response string
92+
if _, err := fmt.Fscanln(in, &response); err != nil {
93+
return false
94+
}
95+
return response == "YES"
96+
}
97+
98+
func runRotate(o *awsCredsRotateOptions) error {
99+
ctx := context.TODO()
100+
101+
rc, err := o.identifyCluster()
102+
if err != nil {
103+
return err
104+
}
105+
defer rc.ocmConn.Close()
106+
107+
if o.rotateCcsAdmin && !rc.isCCS {
108+
return fmt.Errorf("--ccs-admin specified but cluster is not CCS/BYOC")
109+
}
110+
111+
if o.refreshSecrets {
112+
if err := o.resolveForCRSecrets(ctx, rc); err != nil {
113+
return err
114+
}
115+
report, err := controller.DiagnoseCRSecrets(ctx, rc.hiveClient, rc.managedClient, rc.claimName, rc.account, o.Out)
116+
if err != nil {
117+
return err
118+
}
119+
controller.RenderCredRequestTable(report, o.Out)
120+
return runRefreshSecrets(o, rc, report)
121+
}
122+
123+
if err := o.resolveCluster(ctx, rc); err != nil {
124+
return err
125+
}
126+
127+
o.log.Info("Running pre-rotation diagnostic snapshot")
128+
input := rc.toCredsInput(o.log, o.Out)
129+
report, err := controller.DiagnoseCredentials(ctx, input)
130+
if err != nil {
131+
return err
132+
}
133+
controller.RenderReport(report, o.Out)
134+
135+
if !report.AllPermissionsOK {
136+
o.log.Warn("IAM permission check detected issues")
137+
red := color.New(color.FgRed).SprintFunc()
138+
fmt.Fprintf(o.Out, "\n%s Pre-flight permission checks detected issues.\n", red("[WARN]"))
139+
fmt.Fprintln(o.Out, "Proceeding may result in failures during rotation or CR secret recreation.")
140+
if !confirmStrictYES(o.In, o.Out) {
141+
o.log.Info("Operation cancelled by user")
142+
return nil
143+
}
144+
}
145+
146+
if o.dryRun {
147+
o.log.Info("Dry-run mode — no changes will be made")
148+
}
149+
150+
rotateInput := &controller.RotateSecretInput{
151+
AccountCRName: rc.claimName,
152+
Account: rc.account,
153+
OsdManagedAdminUsername: rc.adminUsername,
154+
UpdateManagedAdminCreds: o.rotateManagedAdmin,
155+
UpdateCcsCreds: o.rotateCcsAdmin,
156+
DryRun: o.dryRun,
157+
AwsClient: rc.awsClient,
158+
HiveKubeClient: rc.hiveClient,
159+
ManagedClusterClient: rc.managedClient,
160+
Report: report,
161+
Log: o.log,
162+
Out: o.Out,
163+
}
164+
165+
if !o.dryRun {
166+
o.log.Warn("Credential rotation will modify IAM keys and Hive secrets")
167+
fmt.Fprintln(o.Out, "\nProceed with credential rotation?")
168+
if !utils.ConfirmPrompt() {
169+
o.log.Info("Rotation cancelled by user")
170+
return nil
171+
}
172+
}
173+
174+
o.log.Info("Starting credential rotation")
175+
if err := controller.RotateSecret(ctx, rotateInput); err != nil {
176+
o.log.WithError(err).Error("Credential rotation failed")
177+
return err
178+
}
179+
o.log.Info("Credential rotation completed successfully")
180+
return nil
181+
}
182+
183+
func runRefreshSecrets(o *awsCredsRotateOptions, rc *resolvedCluster, report *controller.DiagnosticReport) error {
184+
ctx := context.TODO()
185+
186+
if rc.managedClient == nil {
187+
return fmt.Errorf("managed cluster client not available — cannot refresh secrets")
188+
}
189+
190+
if o.dryRun {
191+
o.log.Info("Dry-run mode — no secrets will be deleted")
192+
fmt.Fprintln(o.Out, "\n[Dry Run] Would delete and recreate all CredentialRequest secrets.")
193+
fmt.Fprintln(o.Out, "[Dry Run] No AWS keys or Hive secrets would be modified.")
194+
return nil
195+
}
196+
197+
fmt.Fprintf(o.Out, "\nThis will delete %d CredentialRequest secret(s) so CCO recreates them.\n", len(report.CredRequests))
198+
fmt.Fprintln(o.Out, "No AWS keys or Hive secrets will be modified.")
199+
fmt.Fprintln(o.Out, "\nProceed with secret refresh?")
200+
if !utils.ConfirmPrompt() {
201+
o.log.Info("Refresh cancelled by user")
202+
return nil
203+
}
204+
205+
o.log.Info("Deleting credential secrets for CCO to recreate")
206+
if err := controller.DeleteCredentialSecrets(ctx, rc.managedClient, o.Out); err != nil {
207+
o.log.WithError(err).Error("Failed to refresh credential secrets")
208+
return err
209+
}
210+
211+
o.log.Info("Credential secret refresh completed successfully")
212+
return nil
213+
}

cmd/account/aws-creds-snapshot.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package account
2+
3+
import (
4+
"context"
5+
6+
"github.com/openshift/osdctl/pkg/controller"
7+
"github.com/spf13/cobra"
8+
"k8s.io/cli-runtime/pkg/genericclioptions"
9+
cmdutil "k8s.io/kubectl/pkg/cmd/util"
10+
)
11+
12+
type awsCredsSnapshotOptions struct {
13+
awsCredsOptions
14+
crSecretsOnly bool
15+
}
16+
17+
func newCmdAWSCredsSnapshot(streams genericclioptions.IOStreams) *cobra.Command {
18+
ops := &awsCredsSnapshotOptions{
19+
awsCredsOptions: awsCredsOptions{IOStreams: streams, log: newAWSCredsLogger()},
20+
}
21+
22+
cmd := &cobra.Command{
23+
Use: "snapshot -C <cluster-id> --reason <reason> [flags]",
24+
Short: "Show a read-only credential status report for a cluster",
25+
Long: `Produces a diagnostic report of AWS IAM credentials including:
26+
- IAM access keys and which Hive secrets reference them
27+
- CredentialRequest secrets and whether they need refresh
28+
- IAM permission simulation (SCP/policy restriction detection)
29+
30+
Use --cr-secrets to show only the CredentialRequest secrets table.
31+
32+
This is a read-only operation — no credentials are modified.
33+
34+
AWS credentials are obtained via backplane by default. Use --aws-profile
35+
to override with a local AWS profile and manual role chaining.`,
36+
Example: ` # Full credential status report
37+
osdctl account aws-creds snapshot -C $CLUSTER_ID --reason "$JIRA_TICKET"
38+
39+
# Only show CredentialRequest secret status
40+
osdctl account aws-creds snapshot -C $CLUSTER_ID --reason "$JIRA_TICKET" --cr-secrets
41+
42+
# With staging cluster and production hive
43+
osdctl account aws-creds snapshot -C $CLUSTER_ID --reason "$JIRA_TICKET" --hive-ocm-url production`,
44+
DisableAutoGenTag: true,
45+
Run: func(cmd *cobra.Command, args []string) {
46+
cmdutil.CheckErr(ops.validate(cmd, args))
47+
cmdutil.CheckErr(runSnapshot(ops))
48+
},
49+
}
50+
51+
ops.addFlags(cmd)
52+
cmd.Flags().BoolVar(&ops.crSecretsOnly, "cr-secrets", false, "Only show CredentialRequest secrets status")
53+
return cmd
54+
}
55+
56+
func runSnapshot(o *awsCredsSnapshotOptions) error {
57+
ctx := context.TODO()
58+
59+
rc, err := o.identifyCluster()
60+
if err != nil {
61+
return err
62+
}
63+
defer rc.ocmConn.Close()
64+
65+
if o.crSecretsOnly {
66+
if err := o.resolveForCRSecrets(ctx, rc); err != nil {
67+
return err
68+
}
69+
report, err := controller.DiagnoseCRSecrets(ctx, rc.hiveClient, rc.managedClient, rc.claimName, rc.account, o.Out)
70+
if err != nil {
71+
return err
72+
}
73+
controller.RenderCredRequestTable(report, o.Out)
74+
return nil
75+
}
76+
77+
if err := o.resolveCluster(ctx, rc); err != nil {
78+
return err
79+
}
80+
81+
input := rc.toCredsInput(o.log, o.Out)
82+
report, err := controller.DiagnoseCredentials(ctx, input)
83+
if err != nil {
84+
return err
85+
}
86+
controller.RenderReport(report, o.Out)
87+
return nil
88+
}

0 commit comments

Comments
 (0)