mmv1/api/product.go (241 lines of code) (raw):
// Copyright 2024 Google Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"log"
"reflect"
"regexp"
"strings"
"unicode"
"github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product"
"github.com/GoogleCloudPlatform/magic-modules/mmv1/google"
"golang.org/x/exp/slices"
)
// Represents a product to be managed
type Product struct {
// The name of the product's API capitalised in the appropriate places.
// This isn't just the API name because it doesn't meaningfully separate
// words in the api name - "accesscontextmanager" vs "AccessContextManager"
// Example inputs: "Compute", "AccessContextManager"
Name string
// original value of :name before the provider override happens
// same as :name if not overridden in provider
ApiName string `yaml:"api_name,omitempty"`
// Display Name: The full name of the GCP product; eg "Cloud Bigtable"
DisplayName string `yaml:"display_name,omitempty"`
Objects []*Resource `yaml:"objects,omitempty"`
// The list of permission scopes available for the service
// For example: `https://www.googleapis.com/auth/compute`
Scopes []string
// The API versions of this product
Versions []*product.Version
// The base URL for the service API endpoint
// For example: `https://www.googleapis.com/compute/v1/`
BaseUrl string `yaml:"base_url,omitempty"`
// The validator "relative URI" of a resource, relative to the product
// base URL. Specific to defining the resource as a CAI asset.
CaiBaseUrl string
// A function reference designed for the rare case where you
// need to use retries in operation calls. Used for the service api
// as it enables itself (self referential) and can result in occasional
// failures on operation_get. see github.com/hashicorp/terraform-provider-google/issues/9489
OperationRetry string `yaml:"operation_retry,omitempty"`
Async *Async `yaml:"async,omitempty"`
LegacyName string `yaml:"legacy_name,omitempty"`
ClientName string `yaml:"client_name,omitempty"`
}
func (p *Product) UnmarshalYAML(unmarshal func(any) error) error {
type productAlias Product
aliasObj := (*productAlias)(p)
if err := unmarshal(aliasObj); err != nil {
return err
}
p.SetApiName()
p.SetDisplayName()
return nil
}
func (p *Product) Validate() {
if len(p.Name) == 0 {
log.Fatalf("Missing `name` for product")
}
// product names must start with a capital
for i, ch := range p.Name {
if !unicode.IsUpper(ch) {
log.Fatalf("product name `%s` must start with a capital letter.", p.Name)
}
if i == 0 {
break
}
}
if len(p.Scopes) == 0 {
log.Fatalf("Missing `scopes` for product %s", p.Name)
}
if p.Versions == nil {
log.Fatalf("Missing `versions` for product %s", p.Name)
}
for _, v := range p.Versions {
v.Validate(p.Name)
}
if p.Async != nil {
p.Async.Validate()
}
}
// ====================
// Custom Setters
// ====================
func (p *Product) SetApiName() {
// The name of the product's API; "compute", "accesscontextmanager"
p.ApiName = strings.ToLower(p.Name)
}
// The product full name is the "display name" in string form intended for
// users to read in documentation; "Google Compute Engine", "Cloud Bigtable"
func (p *Product) SetDisplayName() {
if p.DisplayName == "" {
p.DisplayName = google.SpaceSeparated(p.Name)
}
}
// ====================
// Version-related methods
// ====================
// Most general version that exists for the product
// If GA is present, use that, else beta, else alpha
func (p Product) lowestVersion() *product.Version {
for _, orderedVersionName := range product.ORDER {
for _, productVersion := range p.Versions {
if orderedVersionName == productVersion.Name {
return productVersion
}
}
}
log.Fatalf("Unable to find lowest version for product %s", p.DisplayName)
return nil
}
func (p Product) versionObj(name string) *product.Version {
for _, v := range p.Versions {
if v.Name == name {
return v
}
}
log.Fatalf("API version '%s' does not exist for product '%s'", name, p.Name)
return nil
}
// Get the version of the object specified by the version given if present
// Or else fall back to the closest version in the chain defined by product.ORDER
func (p Product) VersionObjOrClosest(name string) *product.Version {
if p.ExistsAtVersion(name) {
return p.versionObj(name)
}
// versions should fall back to the closest version to them that exists
if name == "" {
name = product.ORDER[0]
}
lowerVersions := make([]string, 0)
for _, v := range product.ORDER {
lowerVersions = append(lowerVersions, v)
if v == name {
break
}
}
for i := len(lowerVersions) - 1; i >= 0; i-- {
if p.ExistsAtVersion(lowerVersions[i]) {
return p.versionObj(lowerVersions[i])
}
}
log.Fatalf("Could not find object for version %s and product %s", name, p.DisplayName)
return nil
}
func (p *Product) ExistsAtVersionOrLower(name string) bool {
if !slices.Contains(product.ORDER, name) {
return false
}
for i := 0; i <= slices.Index(product.ORDER, name); i++ {
if p.ExistsAtVersion(product.ORDER[i]) {
return true
}
}
return false
}
func (p *Product) ExistsAtVersion(name string) bool {
for _, v := range p.Versions {
if v.Name == name {
return true
}
}
return false
}
func (p *Product) SetPropertiesBasedOnVersion(version *product.Version) {
p.BaseUrl = version.BaseUrl
p.CaiBaseUrl = version.CaiBaseUrl
}
func (p *Product) TerraformName() string {
if p.LegacyName != "" {
return google.Underscore(p.LegacyName)
}
return google.Underscore(p.Name)
}
func (p *Product) ServiceBaseUrl() string {
if p.CaiBaseUrl != "" {
return p.CaiBaseUrl
}
return p.BaseUrl
}
func (p *Product) ServiceName() string {
parts := strings.Split(p.ServiceBaseUrl(), "/")
// remove location prefix if present
trimmed, _ := strings.CutPrefix(parts[2], "{{location}}-")
return trimmed
}
var versionRegexp = regexp.MustCompile(`^v[0-9]+|beta`)
func (p *Product) ServiceVersion() string {
parts := strings.Split(p.ServiceBaseUrl(), "/")
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
// stop when we get to the domain name
if strings.Contains(part, ".com") {
break
}
v := versionRegexp.FindString(part)
if v != "" {
return part
}
}
return ""
}
// ====================
// Debugging Methods
// ====================
// Prints a dot notation path to where the field is nested within the parent
// object when called on a property. eg: parent.meta.label.foo
// Redefined on Product to terminate the calls up the parent chain.
func (p Product) Lineage() string {
return p.Name
}
func Merge(self, otherObj reflect.Value) {
selfObj := reflect.Indirect(self)
for i := 0; i < selfObj.NumField(); i++ {
// skip if the override is the "empty" value
emptyOverrideValue := reflect.DeepEqual(reflect.Zero(otherObj.Field(i).Type()).Interface(), otherObj.Field(i).Interface())
if emptyOverrideValue && selfObj.Type().Field(i).Name != "Required" {
continue
}
if selfObj.Field(i).Kind() == reflect.Slice {
DeepMerge(selfObj.Field(i), otherObj.Field(i))
} else {
selfObj.Field(i).Set(otherObj.Field(i))
}
}
}
func DeepMerge(arr1, arr2 reflect.Value) {
if arr1.Len() == 0 {
arr1.Set(arr2)
return
}
if arr2.Len() == 0 {
return
}
// Scopes is an array of standard strings. In which case return the
// version in the overrides. This allows scopes to be removed rather
// than allowing for a merge of the two arrays
if arr1.Index(0).Kind() == reflect.String {
arr1.Set(arr2)
return
}
// Merge any elements that exist in both
for i := 0; i < arr1.Len(); i++ {
currentVal := arr1.Index(i)
pointer := currentVal.Kind() == reflect.Ptr
if pointer {
currentVal = currentVal.Elem()
}
var otherVal reflect.Value
for j := 0; j < arr2.Len(); j++ {
currentName := currentVal.FieldByName("Name").Interface()
tempOtherVal := arr2.Index(j)
if pointer {
tempOtherVal = tempOtherVal.Elem()
}
otherName := tempOtherVal.FieldByName("Name").Interface()
if otherName == currentName {
otherVal = tempOtherVal
break
}
}
if otherVal.IsValid() {
Merge(currentVal, otherVal)
}
}
// Add any elements of arr2 that don't exist in arr1
for i := 0; i < arr2.Len(); i++ {
otherVal := arr2.Index(i)
pointer := otherVal.Kind() == reflect.Ptr
if pointer {
otherVal = otherVal.Elem()
}
found := false
for j := 0; j < arr1.Len(); j++ {
currentVal := arr1.Index(j)
if pointer {
currentVal = currentVal.Elem()
}
currentName := currentVal.FieldByName("Name").Interface()
otherName := otherVal.FieldByName("Name").Interface()
if otherName == currentName {
found = true
break
}
}
if !found {
arr1.Set(reflect.Append(arr1, arr2.Index(i)))
}
}
}