github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/gcp/client.go (about) 1 package gcp 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/pkg/errors" 10 googleoauth "golang.org/x/oauth2/google" 11 "google.golang.org/api/cloudresourcemanager/v3" 12 compute "google.golang.org/api/compute/v1" 13 dns "google.golang.org/api/dns/v1" 14 "google.golang.org/api/googleapi" 15 iam "google.golang.org/api/iam/v1" 16 "google.golang.org/api/option" 17 "google.golang.org/api/serviceusage/v1" 18 "k8s.io/apimachinery/pkg/util/sets" 19 20 gcpconsts "github.com/openshift/installer/pkg/constants/gcp" 21 ) 22 23 //go:generate mockgen -source=./client.go -destination=./mock/gcpclient_generated.go -package=mock 24 25 const defaultTimeout = 2 * time.Minute 26 27 var ( 28 // RequiredBasePermissions is the list of permissions required for an installation. 29 // A list of valid permissions can be found at https://cloud.google.com/iam/docs/understanding-roles. 30 RequiredBasePermissions = []string{} 31 ) 32 33 // API represents the calls made to the API. 34 type API interface { 35 GetNetwork(ctx context.Context, network, project string) (*compute.Network, error) 36 GetMachineType(ctx context.Context, project, zone, machineType string) (*compute.MachineType, error) 37 GetMachineTypeWithZones(ctx context.Context, project, region, machineType string) (*compute.MachineType, sets.Set[string], error) 38 GetPublicDomains(ctx context.Context, project string) ([]string, error) 39 GetDNSZone(ctx context.Context, project, baseDomain string, isPublic bool) (*dns.ManagedZone, error) 40 GetDNSZoneByName(ctx context.Context, project, zoneName string) (*dns.ManagedZone, error) 41 GetSubnetworks(ctx context.Context, network, project, region string) ([]*compute.Subnetwork, error) 42 GetProjects(ctx context.Context) (map[string]string, error) 43 GetRegions(ctx context.Context, project string) ([]string, error) 44 GetRecordSets(ctx context.Context, project, zone string) ([]*dns.ResourceRecordSet, error) 45 GetZones(ctx context.Context, project, filter string) ([]*compute.Zone, error) 46 GetEnabledServices(ctx context.Context, project string) ([]string, error) 47 GetServiceAccount(ctx context.Context, project, serviceAccount string) (string, error) 48 GetCredentials() *googleoauth.Credentials 49 GetImage(ctx context.Context, name string, project string) (*compute.Image, error) 50 GetProjectPermissions(ctx context.Context, project string, permissions []string) (sets.Set[string], error) 51 GetProjectByID(ctx context.Context, project string) (*cloudresourcemanager.Project, error) 52 ValidateServiceAccountHasPermissions(ctx context.Context, project string, permissions []string) (bool, error) 53 GetProjectTags(ctx context.Context, projectID string) (sets.Set[string], error) 54 GetNamespacedTagValue(ctx context.Context, tagNamespacedName string) (*cloudresourcemanager.TagValue, error) 55 } 56 57 // Client makes calls to the GCP API. 58 type Client struct { 59 ssn *Session 60 } 61 62 // NewClient initializes a client with a session. 63 func NewClient(ctx context.Context) (*Client, error) { 64 ssn, err := GetSession(ctx) 65 if err != nil { 66 return nil, errors.Wrap(err, "failed to get session") 67 } 68 69 client := &Client{ 70 ssn: ssn, 71 } 72 return client, nil 73 } 74 75 // GetMachineType uses the GCP Compute Service API to get the specified machine type. 76 func (c *Client) GetMachineType(ctx context.Context, project, zone, machineType string) (*compute.MachineType, error) { 77 svc, err := c.getComputeService(ctx) 78 if err != nil { 79 return nil, err 80 } 81 82 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 83 defer cancel() 84 req, err := svc.MachineTypes.Get(project, zone, machineType).Context(ctx).Do() 85 if err != nil { 86 return nil, err 87 } 88 89 return req, nil 90 } 91 92 // GetMachineTypeList retrieves the machine type with the specified fields. 93 func GetMachineTypeList(ctx context.Context, svc *compute.Service, project, region, machineType, fields string) ([]*compute.MachineType, error) { 94 var machines []*compute.MachineType 95 96 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 97 defer cancel() 98 99 filter := fmt.Sprintf("name = \"%s\" AND zone : %s-*", machineType, region) 100 req := svc.MachineTypes.AggregatedList(project).Filter(filter).Context(ctx) 101 if len(fields) > 0 { 102 req.Fields(googleapi.Field(fields)) 103 } 104 105 err := req.Pages(ctx, func(page *compute.MachineTypeAggregatedList) error { 106 for _, scopedList := range page.Items { 107 machines = append(machines, scopedList.MachineTypes...) 108 } 109 return nil 110 }) 111 112 return machines, err 113 } 114 115 // GetMachineTypeWithZones retrieves the specified machine type and the zones in which it is available. 116 func (c *Client) GetMachineTypeWithZones(ctx context.Context, project, region, machineType string) (*compute.MachineType, sets.Set[string], error) { 117 svc, err := c.getComputeService(ctx) 118 if err != nil { 119 return nil, nil, err 120 } 121 122 pz, err := GetZones(ctx, svc, project, fmt.Sprintf("region eq .*%s", region)) 123 if err != nil { 124 return nil, nil, err 125 } 126 projZones := sets.New[string]() 127 for _, zone := range pz { 128 projZones.Insert(zone.Name) 129 } 130 131 machines, err := GetMachineTypeList(ctx, svc, project, region, machineType, "") 132 if err != nil { 133 return nil, nil, err 134 } 135 136 // Custom machine types are not included in aggregated lists, so let's try 137 // to get the machine type directly before returning an error. Also 138 // fallback to all the zones in the project 139 if len(machines) == 0 { 140 cctx, cancel := context.WithTimeout(ctx, defaultTimeout) 141 defer cancel() 142 machine, err := svc.MachineTypes.Get(project, pz[0].Name, machineType).Context(cctx).Do() 143 if err != nil { 144 return nil, nil, fmt.Errorf("failed to fetch instance type: %w", err) 145 } 146 return machine, projZones, nil 147 } 148 149 zones := sets.New[string]() 150 for _, machine := range machines { 151 zones.Insert(machine.Zone) 152 } 153 // Restrict to zones avaialable in the project 154 zones = zones.Intersection(projZones) 155 156 return machines[0], zones, nil 157 } 158 159 // GetNetwork uses the GCP Compute Service API to get a network by name from a project. 160 func (c *Client) GetNetwork(ctx context.Context, network, project string) (*compute.Network, error) { 161 svc, err := c.getComputeService(ctx) 162 if err != nil { 163 return nil, err 164 } 165 166 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 167 defer cancel() 168 res, err := svc.Networks.Get(project, network).Context(ctx).Do() 169 if err != nil { 170 return nil, errors.Wrapf(err, "failed to get network %s", network) 171 } 172 return res, nil 173 } 174 175 // GetPublicDomains returns all of the domains from among the project's public DNS zones. 176 func (c *Client) GetPublicDomains(ctx context.Context, project string) ([]string, error) { 177 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 178 defer cancel() 179 180 svc, err := c.getDNSService(ctx) 181 if err != nil { 182 return []string{}, err 183 } 184 185 var publicZones []string 186 req := svc.ManagedZones.List(project).Context(ctx) 187 if err := req.Pages(ctx, func(page *dns.ManagedZonesListResponse) error { 188 for _, v := range page.ManagedZones { 189 if v.Visibility != "private" { 190 publicZones = append(publicZones, strings.TrimSuffix(v.DnsName, ".")) 191 } 192 } 193 return nil 194 }); err != nil { 195 return publicZones, err 196 } 197 return publicZones, nil 198 } 199 200 // GetDNSZoneByName returns a DNS zone matching the `zoneName` if the DNS zone exists 201 // and can be seen (correct permissions for a private zone) in the project. 202 func (c *Client) GetDNSZoneByName(ctx context.Context, project, zoneName string) (*dns.ManagedZone, error) { 203 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 204 defer cancel() 205 206 svc, err := c.getDNSService(ctx) 207 if err != nil { 208 return nil, err 209 } 210 returnedZone, err := svc.ManagedZones.Get(project, zoneName).Context(ctx).Do() 211 if err != nil { 212 return nil, errors.Wrap(err, "failed to get DNS Zones") 213 } 214 return returnedZone, nil 215 } 216 217 // GetDNSZone returns a DNS zone for a basedomain. 218 func (c *Client) GetDNSZone(ctx context.Context, project, baseDomain string, isPublic bool) (*dns.ManagedZone, error) { 219 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 220 defer cancel() 221 222 svc, err := c.getDNSService(ctx) 223 if err != nil { 224 return nil, err 225 } 226 if !strings.HasSuffix(baseDomain, ".") { 227 baseDomain = fmt.Sprintf("%s.", baseDomain) 228 } 229 req := svc.ManagedZones.List(project).DnsName(baseDomain).Context(ctx) 230 var res *dns.ManagedZone 231 if err := req.Pages(ctx, func(page *dns.ManagedZonesListResponse) error { 232 for idx, v := range page.ManagedZones { 233 if v.Visibility != "private" && isPublic { 234 res = page.ManagedZones[idx] 235 } else if v.Visibility == "private" && !isPublic { 236 res = page.ManagedZones[idx] 237 } 238 } 239 return nil 240 }); err != nil { 241 return nil, errors.Wrap(err, "failed to list DNS Zones") 242 } 243 if res == nil { 244 if isPublic { 245 return nil, errors.New("no matching public DNS Zone found") 246 } 247 // A Private DNS Zone may be created (if the correct permissions exist) 248 return nil, nil 249 } 250 return res, nil 251 } 252 253 // GetRecordSets returns all the records for a DNS zone. 254 func (c *Client) GetRecordSets(ctx context.Context, project, zone string) ([]*dns.ResourceRecordSet, error) { 255 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 256 defer cancel() 257 258 svc, err := c.getDNSService(ctx) 259 if err != nil { 260 return nil, err 261 } 262 263 req := svc.ResourceRecordSets.List(project, zone).Context(ctx) 264 var rrSets []*dns.ResourceRecordSet 265 if err := req.Pages(ctx, func(page *dns.ResourceRecordSetsListResponse) error { 266 rrSets = append(rrSets, page.Rrsets...) 267 return nil 268 }); err != nil { 269 return nil, err 270 } 271 return rrSets, nil 272 } 273 274 // GetSubnetworks uses the GCP Compute Service API to retrieve all subnetworks in a given network. 275 func (c *Client) GetSubnetworks(ctx context.Context, network, project, region string) ([]*compute.Subnetwork, error) { 276 svc, err := c.getComputeService(ctx) 277 if err != nil { 278 return nil, err 279 } 280 281 filter := fmt.Sprintf("network eq .*%s", network) 282 req := svc.Subnetworks.List(project, region).Filter(filter) 283 var res []*compute.Subnetwork 284 285 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 286 defer cancel() 287 288 if err := req.Pages(ctx, func(page *compute.SubnetworkList) error { 289 res = append(res, page.Items...) 290 return nil 291 }); err != nil { 292 return nil, err 293 } 294 return res, nil 295 } 296 297 func (c *Client) getComputeService(ctx context.Context) (*compute.Service, error) { 298 svc, err := compute.NewService(ctx, option.WithCredentials(c.ssn.Credentials)) 299 if err != nil { 300 return nil, errors.Wrap(err, "failed to create compute service") 301 } 302 return svc, nil 303 } 304 305 func (c *Client) getDNSService(ctx context.Context) (*dns.Service, error) { 306 svc, err := dns.NewService(ctx, option.WithCredentials(c.ssn.Credentials)) 307 if err != nil { 308 return nil, errors.Wrap(err, "failed to create dns service") 309 } 310 return svc, nil 311 } 312 313 // GetProjects gets the list of project names and ids associated with the current user in the form 314 // of a map whose keys are ids and values are names. 315 func (c *Client) GetProjects(ctx context.Context) (map[string]string, error) { 316 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 317 defer cancel() 318 319 svc, err := c.getCloudResourceService(ctx) 320 if err != nil { 321 return nil, err 322 } 323 324 req := svc.Projects.Search() 325 projects := make(map[string]string) 326 if err := req.Pages(ctx, func(page *cloudresourcemanager.SearchProjectsResponse) error { 327 for _, project := range page.Projects { 328 projects[project.ProjectId] = project.Name 329 } 330 return nil 331 }); err != nil { 332 return nil, err 333 } 334 return projects, nil 335 } 336 337 // GetProjectByID retrieves the project specified by its ID. 338 func (c *Client) GetProjectByID(ctx context.Context, project string) (*cloudresourcemanager.Project, error) { 339 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 340 defer cancel() 341 342 svc, err := c.getCloudResourceService(ctx) 343 if err != nil { 344 return nil, err 345 } 346 347 return svc.Projects.Get(fmt.Sprintf(gcpconsts.ProjectNameFmt, project)).Context(ctx).Do() 348 } 349 350 // GetRegions gets the regions that are valid for the project. An error is returned when unsuccessful 351 func (c *Client) GetRegions(ctx context.Context, project string) ([]string, error) { 352 svc, err := c.getComputeService(ctx) 353 if err != nil { 354 return nil, err 355 } 356 357 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 358 defer cancel() 359 gcpRegionsList, err := svc.Regions.List(project).Context(ctx).Do() 360 if err != nil { 361 return nil, errors.Wrapf(err, "failed to get regions for project") 362 } 363 364 computeRegions := make([]string, len(gcpRegionsList.Items)) 365 for _, region := range gcpRegionsList.Items { 366 computeRegions = append(computeRegions, region.Name) 367 } 368 369 return computeRegions, nil 370 } 371 372 // GetZones uses the GCP Compute Service API to get a list of zones from a project. 373 func GetZones(ctx context.Context, svc *compute.Service, project, filter string) ([]*compute.Zone, error) { 374 req := svc.Zones.List(project) 375 if filter != "" { 376 req = req.Filter(filter) 377 } 378 379 zones := []*compute.Zone{} 380 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 381 defer cancel() 382 if err := req.Pages(ctx, func(page *compute.ZoneList) error { 383 zones = append(zones, page.Items...) 384 return nil 385 }); err != nil { 386 return nil, errors.Wrapf(err, "failed to get zones from project %s", project) 387 } 388 389 return zones, nil 390 } 391 392 // GetZones uses the GCP Compute Service API to get a list of zones from a project. 393 func (c *Client) GetZones(ctx context.Context, project, filter string) ([]*compute.Zone, error) { 394 svc, err := c.getComputeService(ctx) 395 if err != nil { 396 return nil, err 397 } 398 399 return GetZones(ctx, svc, project, filter) 400 } 401 402 func (c *Client) getCloudResourceService(ctx context.Context) (*cloudresourcemanager.Service, error) { 403 svc, err := cloudresourcemanager.NewService(ctx, option.WithCredentials(c.ssn.Credentials)) 404 if err != nil { 405 return nil, errors.Wrap(err, "failed to create cloud resource service") 406 } 407 return svc, nil 408 } 409 410 // GetEnabledServices gets the list of enabled services for a project. 411 func (c *Client) GetEnabledServices(ctx context.Context, project string) ([]string, error) { 412 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 413 defer cancel() 414 415 svc, err := c.getServiceUsageService(ctx) 416 if err != nil { 417 return nil, err 418 } 419 420 // List accepts a parent, which includes the type of resource with the id. 421 parent := fmt.Sprintf("projects/%s", project) 422 req := svc.Services.List(parent).Filter("state:ENABLED") 423 var services []string 424 if err := req.Pages(ctx, func(page *serviceusage.ListServicesResponse) error { 425 for _, service := range page.Services { 426 //services are listed in the form of project/services/serviceName 427 index := strings.LastIndex(service.Name, "/") 428 services = append(services, service.Name[index+1:]) 429 } 430 return nil 431 }); err != nil { 432 return nil, err 433 } 434 return services, nil 435 } 436 437 func (c *Client) getServiceUsageService(ctx context.Context) (*serviceusage.Service, error) { 438 svc, err := serviceusage.NewService(ctx, option.WithCredentials(c.ssn.Credentials)) 439 if err != nil { 440 return nil, errors.Wrap(err, "failed to create service usage service") 441 } 442 return svc, nil 443 } 444 445 // GetServiceAccount retrieves a service account from a project if it exists. 446 func (c *Client) GetServiceAccount(ctx context.Context, project, serviceAccount string) (string, error) { 447 svc, err := iam.NewService(ctx) 448 if err != nil { 449 return "", errors.Wrapf(err, "failed create IAM service") 450 } 451 452 ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) 453 defer cancel() 454 455 fullServiceAccountPath := fmt.Sprintf("projects/%s/serviceAccounts/%s", project, serviceAccount) 456 rsp, err := svc.Projects.ServiceAccounts.Get(fullServiceAccountPath).Context(ctx).Do() 457 if err != nil { 458 return "", errors.Wrapf(err, fmt.Sprintf("failed to find resource %s", fullServiceAccountPath)) 459 } 460 return rsp.Name, nil 461 } 462 463 // GetCredentials returns the credentials used to authenticate the GCP session. 464 func (c *Client) GetCredentials() *googleoauth.Credentials { 465 return c.ssn.Credentials 466 } 467 468 // GetImage returns the marketplace image specified by the user. 469 func (c *Client) GetImage(ctx context.Context, name string, project string) (*compute.Image, error) { 470 svc, err := c.getComputeService(ctx) 471 if err != nil { 472 return nil, err 473 } 474 475 ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) 476 defer cancel() 477 478 return svc.Images.Get(project, name).Context(ctx).Do() 479 } 480 481 func (c *Client) getPermissions(ctx context.Context, project string, permissions []string) ([]string, error) { 482 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 483 defer cancel() 484 485 service, err := c.getCloudResourceService(ctx) 486 if err != nil { 487 return nil, errors.Wrapf(err, "failed to get cloud resource manager service") 488 } 489 490 projectsService := cloudresourcemanager.NewProjectsService(service) 491 rb := &cloudresourcemanager.TestIamPermissionsRequest{Permissions: permissions} 492 response, err := projectsService.TestIamPermissions(fmt.Sprintf(gcpconsts.ProjectNameFmt, project), rb).Context(ctx).Do() 493 if err != nil { 494 return nil, errors.Wrapf(err, "failed to get Iam permissions") 495 } 496 497 return response.Permissions, nil 498 } 499 500 // GetProjectPermissions consumes a set of permissions and returns the set of found permissions for the service 501 // account (in the provided project). A list of valid permissions can be found at 502 // https://cloud.google.com/iam/docs/understanding-roles. 503 func (c *Client) GetProjectPermissions(ctx context.Context, project string, permissions []string) (sets.Set[string], error) { 504 validPermissions, err := c.getPermissions(ctx, project, permissions) 505 if err != nil { 506 return nil, err 507 } 508 return sets.New[string](validPermissions...), nil 509 } 510 511 // ValidateServiceAccountHasPermissions compares the permissions to the set returned from the GCP API. Returns true 512 // if all permissions are available to the service account in the project. 513 func (c *Client) ValidateServiceAccountHasPermissions(ctx context.Context, project string, permissions []string) (bool, error) { 514 validPermissions, err := c.GetProjectPermissions(ctx, project, permissions) 515 if err != nil { 516 return false, err 517 } 518 return validPermissions.Len() == len(permissions), nil 519 } 520 521 // GetProjectTags returns the list of effective tags attached to the provided project resource. 522 func (c *Client) GetProjectTags(ctx context.Context, projectID string) (sets.Set[string], error) { 523 service, err := c.getCloudResourceService(ctx) 524 if err != nil { 525 return nil, fmt.Errorf("failed to create cloud resource service: %w", err) 526 } 527 528 effectiveTags := sets.New[string]() 529 effectiveTagsService := cloudresourcemanager.NewEffectiveTagsService(service) 530 effectiveTagsRequest := effectiveTagsService.List(). 531 Context(ctx). 532 Parent(fmt.Sprintf(gcpconsts.ProjectParentPathFmt, projectID)) 533 534 if err := effectiveTagsRequest.Pages(ctx, func(page *cloudresourcemanager.ListEffectiveTagsResponse) error { 535 for _, effectiveTag := range page.EffectiveTags { 536 effectiveTags.Insert(effectiveTag.NamespacedTagValue) 537 } 538 return nil 539 }); err != nil { 540 return nil, fmt.Errorf("failed to fetch tags attached to %s project: %w", projectID, err) 541 } 542 543 return effectiveTags, nil 544 } 545 546 // GetNamespacedTagValue returns the Tag Value metadata fetched using the tag's NamespacedName. 547 func (c *Client) GetNamespacedTagValue(ctx context.Context, tagNamespacedName string) (*cloudresourcemanager.TagValue, error) { 548 service, err := c.getCloudResourceService(ctx) 549 if err != nil { 550 return nil, fmt.Errorf("failed to create cloud resource service: %w", err) 551 } 552 553 tagValuesService := cloudresourcemanager.NewTagValuesService(service) 554 555 tagValue, err := tagValuesService.GetNamespaced(). 556 Context(ctx). 557 Name(tagNamespacedName). 558 Do() 559 560 if err != nil { 561 return nil, fmt.Errorf("failed to fetch %s tag value: %w", tagNamespacedName, err) 562 } 563 564 return tagValue, nil 565 }