Skip to content

Commit dfb52ff

Browse files
miguelhbritoMiguel Pereira
andauthored
pool usage after update (#888)
Co-authored-by: Miguel Pereira <[email protected]>
1 parent 3ce28c3 commit dfb52ff

File tree

7 files changed

+342
-28
lines changed

7 files changed

+342
-28
lines changed

cmd/ocm/gcp/flag_descriptions.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ manual: Commands necessary to modify GCP resources will be output
88
as a script to be run manually.
99
`
1010

11-
targetDirFlagDescription = `Directory to place generated files (defaults to current directory)`
12-
versionFlagDescription = `Version of OpenShift to configure the WIF resources for`
13-
federatedProjectFlagDescription = `ID of the Google cloud project that will host the WIF pool`
11+
targetDirFlagDescription = `Directory to place generated files (defaults to current directory)`
12+
versionFlagDescription = `Version of OpenShift to configure the WIF resources for`
13+
federatedProjectFlagDescription = `ID of the Google cloud project that will host the WIF pool`
14+
skipFederatedProjectVerificationFlagDescription = `Skip federated project verification`
1415
)

cmd/ocm/gcp/gcp-client-shim.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"cloud.google.com/go/iam/admin/apiv1/adminpb"
1212
"github.com/googleapis/gax-go/v2/apierror"
13+
sdk "github.com/openshift-online/ocm-sdk-go"
1314
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
1415
"github.com/pkg/errors"
1516
cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1"
@@ -26,6 +27,12 @@ const (
2627
IamApiRetrySeconds = 600
2728
)
2829

30+
const (
31+
// PromQL time range in minutes
32+
newPoolUsageTimeRange = 5
33+
oldPoolUsageTimeRange = 2
34+
)
35+
2936
const (
3037
impersonatorRole = "roles/iam.serviceAccountTokenCreator"
3138
workloadIdentityUserRole = "roles/iam.workloadIdentityUser"
@@ -43,6 +50,9 @@ type GcpClientWifConfigShim interface {
4350
DeleteServiceAccounts(ctx context.Context, log *log.Logger) error
4451
DeleteWorkloadIdentityPool(ctx context.Context, log *log.Logger) error
4552
UnbindServiceAccounts(ctx context.Context, log *log.Logger) error
53+
HasAssociatedClusters(ctx context.Context, connection *sdk.Connection, wifConfigID string) (bool, error)
54+
ValidateNewWifConfigPoolUsage(ctx context.Context, wifConfig *cmv1.WifConfig) error
55+
ValidateOldWifConfigPoolUsage(ctx context.Context, wifConfig *cmv1.WifConfig) error
4656
}
4757

4858
type shim struct {
@@ -1205,3 +1215,94 @@ func (c *shim) UnbindServiceAccounts(
12051215
}
12061216
return nil
12071217
}
1218+
1219+
// checkPoolUsageWithQuery executes a PromQL query and returns true if any metric has a non-zero value
1220+
func (c *shim) checkPoolUsageWithQuery(ctx context.Context, projectId string, query string) (bool, error) {
1221+
response, err := c.gcpClient.QueryPrometheusTimeSeries(ctx, projectId, query)
1222+
if err != nil {
1223+
return false, errors.Wrapf(err, "failed to execute prometheus query")
1224+
}
1225+
1226+
// Check if the query was successful
1227+
if response.Status != "success" {
1228+
return false, errors.Errorf("prometheus query failed with status: %s", response.Status)
1229+
}
1230+
1231+
// Check if there are any results
1232+
if len(response.Data.Result) == 0 {
1233+
return false, nil
1234+
}
1235+
1236+
// Check if any result has a non-zero value
1237+
for _, result := range response.Data.Result {
1238+
value, err := result.GetFloatValue()
1239+
if err != nil {
1240+
continue
1241+
}
1242+
if value > 0 {
1243+
return true, nil
1244+
}
1245+
}
1246+
1247+
return false, nil
1248+
}
1249+
1250+
// HasAssociatedClusters checks if a WIF config has any clusters associated with it
1251+
func (c *shim) HasAssociatedClusters(
1252+
ctx context.Context,
1253+
connection *sdk.Connection,
1254+
wifConfigID string,
1255+
) (bool, error) {
1256+
searchQuery := fmt.Sprintf("gcp.authentication.wif_config_id = '%s'", wifConfigID)
1257+
response, err := connection.ClustersMgmt().V1().Clusters().
1258+
List().
1259+
Search(searchQuery).
1260+
Send()
1261+
if err != nil {
1262+
return false, errors.Wrapf(err, "failed to search for clusters using wif-config %s", wifConfigID)
1263+
}
1264+
1265+
return response.Size() > 0, nil
1266+
}
1267+
1268+
// ValidateNewWifConfigPoolUsage validates that a WIF config has recent pool usage
1269+
func (c *shim) ValidateNewWifConfigPoolUsage(ctx context.Context, wifConfig *cmv1.WifConfig) error {
1270+
projectId := wifConfig.Gcp().FederatedProjectId()
1271+
query := fmt.Sprintf(
1272+
WifQuery,
1273+
wifConfig.Gcp().WorkloadIdentityPool().PoolId(),
1274+
newPoolUsageTimeRange,
1275+
)
1276+
1277+
hasUsage, err := c.checkPoolUsageWithQuery(ctx, projectId, query)
1278+
if err != nil {
1279+
return errors.Wrapf(err, "failed to check workload identity pool usage")
1280+
}
1281+
1282+
if !hasUsage {
1283+
return errors.Errorf("wif-config pool '%s' has no recent traffic", wifConfig.Gcp().WorkloadIdentityPool().PoolId())
1284+
}
1285+
1286+
return nil
1287+
}
1288+
1289+
// ValidateOldWifConfigPoolUsage validates if the old WIF config pool is still being used
1290+
func (c *shim) ValidateOldWifConfigPoolUsage(ctx context.Context, wifConfig *cmv1.WifConfig) error {
1291+
projectId := wifConfig.Gcp().ProjectId()
1292+
query := fmt.Sprintf(
1293+
WifQuery,
1294+
wifConfig.Gcp().WorkloadIdentityPool().PoolId(),
1295+
oldPoolUsageTimeRange,
1296+
)
1297+
1298+
hasUsage, err := c.checkPoolUsageWithQuery(ctx, projectId, query)
1299+
if err != nil {
1300+
return errors.Wrapf(err, "failed to check workload identity pool usage")
1301+
}
1302+
1303+
if hasUsage {
1304+
return errors.Errorf("wif-config pool '%s' is still being used", wifConfig.Gcp().WorkloadIdentityPool().PoolId())
1305+
}
1306+
1307+
return nil
1308+
}

cmd/ocm/gcp/gcp.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import (
55
)
66

77
type options struct {
8-
Interactive bool
9-
Mode string
10-
Name string
11-
OpenshiftVersion string
12-
Project string
13-
FederatedProject string
14-
Region string
15-
RolePrefix string
16-
TargetDir string
17-
WorkloadIdentityPool string
18-
WorkloadIdentityProvider string
8+
Interactive bool
9+
Mode string
10+
Name string
11+
OpenshiftVersion string
12+
Project string
13+
FederatedProject string
14+
SkipFederatedProjectVerification bool
15+
Region string
16+
RolePrefix string
17+
TargetDir string
18+
WorkloadIdentityPool string
19+
WorkloadIdentityProvider string
1920
}
2021

2122
// NewGcpCmd implements the "gcp" subcommand for the credentials provisioning

cmd/ocm/gcp/helpers.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
package gcp
22

33
import (
4+
"context"
45
"fmt"
6+
"log"
57
"os"
68
"path/filepath"
79
"regexp"
10+
"strconv"
811

12+
"github.com/openshift-online/ocm-cli/pkg/gcp"
13+
"github.com/openshift-online/ocm-cli/pkg/utils"
14+
sdk "github.com/openshift-online/ocm-sdk-go"
915
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
1016
"github.com/pkg/errors"
1117
)
1218

1319
const (
1420
ModeAuto = "auto"
1521
ModeManual = "manual"
22+
// See reference: https://cloud.google.com/monitoring/api/metrics_gcp_i_o
23+
// iam_googleapis_com:workload_identity_federation_count and workload_identity_federation/key_usage_count
24+
// Both metrics could be used,
25+
// but the first metric was chosen because it was the default metric used by the GCP on Metrics Explorer
26+
WifQuery = `sum(rate(iam_googleapis_com:workload_identity_federation_count{pool_id="%s",result="success"}[%dm]))`
1627
)
1728

1829
var Modes = []string{ModeAuto, ModeManual}
@@ -111,3 +122,80 @@ func getFederatedProjectId(wifConfig *cmv1.WifConfig) string {
111122
}
112123
return wifConfig.Gcp().ProjectId()
113124
}
125+
126+
// updateFederatedProjectIfChanged if federated project was provided,
127+
// and if it differs from main project, update the WIF config with the federated project.
128+
func updateFederatedProjectIfChanged(
129+
ctx context.Context,
130+
gcpClient gcp.GcpClient,
131+
wifBuilder *cmv1.WifConfigBuilder,
132+
wifConfig *cmv1.WifConfig,
133+
federatedProject string,
134+
) (bool, error) {
135+
136+
if federatedProject == "" ||
137+
wifConfig.Gcp().FederatedProjectId() == federatedProject {
138+
return false, nil
139+
}
140+
141+
projectNumInt64, err := gcpClient.ProjectNumberFromId(ctx, federatedProject)
142+
if err != nil {
143+
return false, errors.Wrapf(err, "failed to get GCP project number from project id")
144+
}
145+
146+
wifBuilder.Gcp(cmv1.NewWifGcp().
147+
FederatedProjectId(federatedProject).
148+
FederatedProjectNumber(strconv.FormatInt(projectNumInt64, 10)))
149+
150+
return true, nil
151+
}
152+
153+
func federatedProjectPoolUsageVerification(
154+
ctx context.Context,
155+
log *log.Logger,
156+
connection *sdk.Connection,
157+
wifConfig *cmv1.WifConfig,
158+
gcpShim GcpClientWifConfigShim,
159+
) error {
160+
161+
clustersAssociated, err := gcpShim.HasAssociatedClusters(ctx, connection, wifConfig.ID())
162+
if err != nil {
163+
return errors.Wrapf(err, "failed to check if wif-config '%s' has associated clusters. "+
164+
"Please try again or contact Red Hat support if the issue persists", wifConfig.ID())
165+
}
166+
167+
// Only validate pool usage if there are associated clusters
168+
if !clustersAssociated {
169+
return nil
170+
}
171+
172+
// Validate new pool usage
173+
log.Println("Ensuring new workload identity pool has recent traffic...")
174+
if err := utils.RetryWithBackoffandTimeout(func() (bool, error) {
175+
if validationErr := gcpShim.ValidateNewWifConfigPoolUsage(ctx, wifConfig); validationErr != nil {
176+
return true, validationErr
177+
}
178+
return false, nil
179+
}, IamApiRetrySeconds, log); err != nil {
180+
log.Printf("Timed out verifying workload identity pool usage\n" +
181+
"Cannot determine if old identity pool may be removed." +
182+
"Please consult user documentation for manually checking usage")
183+
return nil
184+
}
185+
186+
// Validate old pool usage
187+
log.Println("Ensuring old workload identity pool is still there and not being used...")
188+
if err := utils.RetryWithBackoffandTimeout(func() (bool, error) {
189+
if err := gcpShim.ValidateOldWifConfigPoolUsage(ctx, wifConfig); err != nil {
190+
return true, err
191+
}
192+
return false, nil
193+
}, IamApiRetrySeconds, log); err != nil {
194+
log.Printf("Timed out verifying old workload identity pool usage\n" +
195+
"Cannot determine if old identity pool may be removed." +
196+
"Please consult user documentation for manually checking usage")
197+
return nil
198+
}
199+
200+
return nil
201+
}

cmd/ocm/gcp/update-wif-config.go

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import (
1616

1717
var (
1818
UpdateWifConfigOpts = options{
19-
Mode: ModeAuto,
20-
TargetDir: "",
21-
OpenshiftVersion: "",
22-
FederatedProject: "",
19+
Mode: ModeAuto,
20+
TargetDir: "",
21+
OpenshiftVersion: "",
22+
FederatedProject: "",
23+
SkipFederatedProjectVerification: false,
2324
}
2425
)
2526

@@ -64,6 +65,13 @@ the wif-config metadata and the GCP resources it represents.`,
6465
federatedProjectFlagDescription,
6566
)
6667

68+
updateWifConfigCmd.PersistentFlags().BoolVar(
69+
&UpdateWifConfigOpts.SkipFederatedProjectVerification,
70+
"skip-federated-project-verification",
71+
false,
72+
skipFederatedProjectVerificationFlagDescription,
73+
)
74+
6775
return updateWifConfigCmd
6876
}
6977

@@ -117,16 +125,15 @@ func updateWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e
117125
return errors.Wrapf(err, "failed to initiate GCP client")
118126
}
119127

120-
if UpdateWifConfigOpts.FederatedProject != "" &&
121-
wifConfig.Gcp().FederatedProjectId() != UpdateWifConfigOpts.FederatedProject {
122-
projectNumInt64, err := gcpClient.ProjectNumberFromId(ctx, UpdateWifConfigOpts.FederatedProject)
123-
if err != nil {
124-
return errors.Wrapf(err, "failed to get GCP project number from project id")
125-
}
126-
127-
wifBuilder.Gcp(cmv1.NewWifGcp().
128-
FederatedProjectId(UpdateWifConfigOpts.FederatedProject).
129-
FederatedProjectNumber(strconv.FormatInt(projectNumInt64, 10)))
128+
wifProjectUpdated, err := updateFederatedProjectIfChanged(
129+
ctx,
130+
gcpClient,
131+
wifBuilder,
132+
wifConfig,
133+
UpdateWifConfigOpts.FederatedProject,
134+
)
135+
if err != nil {
136+
return err
130137
}
131138

132139
updatedWifConfig, err := wifBuilder.Build()
@@ -197,6 +204,16 @@ func updateWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e
197204
"or contact Red Hat support.", wifConfig.ID())
198205
}
199206

207+
// If the federated project was updated and skip verification is not set,
208+
// verify the pool usage
209+
if wifProjectUpdated && !UpdateWifConfigOpts.SkipFederatedProjectVerification {
210+
if err := federatedProjectPoolUsageVerification(
211+
ctx, log, connection, wifConfig, gcpClientWifConfigShim,
212+
); err != nil {
213+
return err
214+
}
215+
}
216+
200217
log.Printf("wif-config '%s' updated successfully.", wifConfig.ID())
201218
return nil
202219
}

0 commit comments

Comments
 (0)