pkg/cloud/utils.go (243 lines of code) (raw):

/* Copyright 2018 Google LLC 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 https://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 cloud import ( "encoding/json" "fmt" "regexp" "strings" "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta" ) var ( domainPrefix = "https://www.googleapis.com" computePrefix = "https://www.googleapis.com/compute" networkServicesPrefix = "https://www.googleapis.com/networkservices" ) // SetAPIDomain sets the root of the URL for the API. The default domain is // "https://www.googleapis.com". func SetAPIDomain(domain string) { domainPrefix = domain computePrefix = domain + "/compute" networkServicesPrefix = domain + "/networkservices" } // ResourceID identifies a GCE resource as parsed from compute resource URL. type ResourceID struct { ProjectID string // APIGroup identifies the API Group of the resource. APIGroup meta.APIGroup Resource string Key *meta.Key } // Equal returns true if two resource IDs are equal. func (r *ResourceID) Equal(other *ResourceID) bool { switch { case r == nil && other == nil: return true case r == nil || other == nil: return false case r.ProjectID != other.ProjectID || r.Resource != other.Resource || r.APIGroup != other.APIGroup: return false case r.Key != nil && other.Key != nil: return *r.Key == *other.Key case r.Key == nil && other.Key == nil: return true default: return false } } // ResourceMapKey is a flat ResourceID that can be used as a key in maps. type ResourceMapKey struct { ProjectID string APIGroup meta.APIGroup Resource string Name string Zone string Region string } func (rk ResourceMapKey) ToID() *ResourceID { return &ResourceID{ ProjectID: rk.ProjectID, APIGroup: rk.APIGroup, Resource: rk.Resource, Key: &meta.Key{Name: rk.Name, Zone: rk.Zone, Region: rk.Region}, } } // MapKey returns a flat key that can be used for referencing in maps. func (r *ResourceID) MapKey() ResourceMapKey { return ResourceMapKey{ ProjectID: r.ProjectID, APIGroup: r.APIGroup, Resource: r.Resource, Name: r.Key.Name, Zone: r.Key.Zone, Region: r.Key.Region, } } // RelativeResourceName returns the relative resource name string // representing this ResourceID. // Deprecated: Use SelfLink instead func (r *ResourceID) RelativeResourceName() string { return RelativeResourceName(r.ProjectID, r.Resource, r.Key) } // ResourcePath returns the resource path representing this ResourceID. // Deprecated: Use SelfLink instead func (r *ResourceID) ResourcePath() string { return ResourcePath(r.Resource, r.Key) } // SelfLink returns a URL representing the resource and defaults to Compute API // Group if no API Group is specified. func (r *ResourceID) SelfLink(ver meta.Version) string { apiGroup := r.APIGroup if apiGroup == "" { apiGroup = meta.APIGroupCompute } return SelfLinkWithGroup(apiGroup, ver, r.ProjectID, r.Resource, r.Key) } func (r *ResourceID) String() string { prefix := fmt.Sprintf("%s:%s", r.Resource, r.ProjectID) if r.APIGroup != "" { prefix = fmt.Sprintf("%s/%s", r.APIGroup, prefix) } switch r.Key.Type() { case meta.Zonal: return fmt.Sprintf("%s/%s/%s", prefix, r.Key.Zone, r.Key.Name) case meta.Regional: return fmt.Sprintf("%s/%s/%s", prefix, r.Key.Region, r.Key.Name) } return fmt.Sprintf("%s/%s", prefix, r.Key.Name) } // apiGroupRegex is used to extract the API Group out of a Resource URL. // This regex expects API Group to be followed ine one of 2 patterns: // <ver>/projects/ path or legacy one <api_group>.googleapis.com/<ver>/projects/. // Unfortunately it cannot predict what comes before the API // group since that is configurable via SetAPIDomain. var apiGroupRegex = regexp.MustCompile(`([a-z]*)(\.googleapis\.com)?\/(alpha|beta|v1|v1alpha1|v1beta1)/projects`) // ParseResourceURL parses resource URLs of the following formats: // // global/<res>/<name> // regions/<region>/<res>/<name> // zones/<zone>/<res>/<name> // projects/<proj> // projects/<proj>/global/<res>/<name> // projects/<proj>/regions/<region>/<res>/<name> // projects/<proj>/zones/<zone>/<res>/<name> // [https://www.googleapis.com/<apigroup>/<ver>]/projects/<proj>/global/<res>/<name> // [https://www.googleapis.com/<apigroup>/<ver>]/projects/<proj>/regions/<region>/<res>/<name> // [https://www.googleapis.com/<apigroup>/<ver>]/projects/<proj>/zones/<zone>/<res>/<name> // [https://<apigroup>.googleapis.com/<ver>]/projects/<proj>/global/<res>/<name> // [https://<apigroup>.googleapis.com/<ver>]/projects/<proj>/regions/<region>/<res>/<name> // [https://<apigroup>.googleapis.com/<ver>]/projects/<proj>/zones/<zone>/<res>/<name> // // Note that ParseResourceURL can't round trip partial paths that do not // include an API Group. func ParseResourceURL(url string) (*ResourceID, error) { matches := apiGroupRegex.FindStringSubmatch(url) apiGroup, err := apiGroupFromMatches(matches) if err != nil { return nil, fmt.Errorf("ParseResourceURL(%q) returned error: %v", url, err) } return parseURL(url, apiGroup) } func apiGroupFromMatches(matches []string) (meta.APIGroup, error) { if len(matches) < 2 { return meta.APIGroup(""), nil } switch matches[1] { case "compute": return meta.APIGroupCompute, nil case "networkservices": return meta.APIGroupNetworkServices, nil } return meta.APIGroup(""), fmt.Errorf("matches does not contain a supported API Group: %v", matches) } func parseURL(url string, apiGroup meta.APIGroup) (*ResourceID, error) { errNotValid := fmt.Errorf("%q is not a valid resource URL", url) // Trim prefix off URL leaving "projects/..." projectsIndex := strings.Index(url, "/projects/") if projectsIndex >= 0 { url = url[projectsIndex+1:] } parts := strings.Split(url, "/") if len(parts) < 2 || len(parts) > 6 { return nil, errNotValid } ret := &ResourceID{APIGroup: apiGroup} scopedName := parts if parts[0] == "projects" { ret.Resource = "projects" ret.ProjectID = parts[1] scopedName = parts[2:] if len(scopedName) == 0 { return ret, nil } } switch scopedName[0] { case "global": if len(scopedName) != 3 { return nil, errNotValid } ret.Resource = scopedName[1] ret.Key = meta.GlobalKey(scopedName[2]) return ret, nil case "regions": switch len(scopedName) { case 2: ret.Resource = "regions" ret.Key = meta.GlobalKey(scopedName[1]) return ret, nil case 4: ret.Resource = scopedName[2] ret.Key = meta.RegionalKey(scopedName[3], scopedName[1]) return ret, nil default: return nil, errNotValid } case "zones": switch len(scopedName) { case 2: ret.Resource = "zones" ret.Key = meta.GlobalKey(scopedName[1]) return ret, nil case 4: ret.Resource = scopedName[2] ret.Key = meta.ZonalKey(scopedName[3], scopedName[1]) return ret, nil default: return nil, errNotValid } } return nil, errNotValid } func copyViaJSON(dest, src interface{}) error { bytes, err := json.Marshal(src) if err != nil { return err } return json.Unmarshal(bytes, dest) } // ResourcePath returns the path starting from the location. // Example: regions/us-central1/subnetworks/my-subnet // Deprecated: Use SelfLinkWithGroup instead func ResourcePath(resource string, key *meta.Key) string { switch resource { case "zones", "regions": return fmt.Sprintf("%s/%s", resource, key.Name) case "projects": return "invalid-resource" } switch key.Type() { case meta.Zonal: return fmt.Sprintf("zones/%s/%s/%s", key.Zone, resource, key.Name) case meta.Regional: return fmt.Sprintf("regions/%s/%s/%s", key.Region, resource, key.Name) case meta.Global: return fmt.Sprintf("global/%s/%s", resource, key.Name) } return "invalid-key-type" } // RelativeResourceName returns the path starting from project. // Example: projects/my-project/regions/us-central1/subnetworks/my-subnet // Deprecated: Use SelfLinkWithGroup instead func RelativeResourceName(project, resource string, key *meta.Key) string { switch resource { case "projects": return fmt.Sprintf("projects/%s", project) default: return fmt.Sprintf("projects/%s/%s", project, ResourcePath(resource, key)) } } // SelfLink returns a URL representing the resource and assumes Compute API Group. // Deprecated: Use SelfLinkWithGroup instead func SelfLink(ver meta.Version, project, resource string, key *meta.Key) string { return SelfLinkWithGroup(meta.APIGroupCompute, ver, project, resource, key) } // SelfLinkWithGroup returns the self link URL for the given object. func SelfLinkWithGroup(apiGroup meta.APIGroup, ver meta.Version, project, resource string, key *meta.Key) string { var prefix string switch apiGroup { case meta.APIGroupCompute: prefix = computePrefix case meta.APIGroupNetworkServices: prefix = networkServicesPrefix default: prefix = domainPrefix + "/invalid-apigroup" } switch ver { case meta.VersionAlpha: prefix = prefix + "/alpha" case meta.VersionBeta: if apiGroup == meta.APIGroupNetworkServices { prefix = prefix + "/v1beta1" } else { prefix = prefix + "/beta" } case meta.VersionGA: prefix = prefix + "/v1" default: prefix = "invalid-version" } return fmt.Sprintf("%s/%s", prefix, RelativeResourceName(project, resource, key)) } // aggregatedListKey return the aggregated list key based on the resource key. func aggregatedListKey(k *meta.Key) string { switch k.Type() { case meta.Regional: return fmt.Sprintf("regions/%s", k.Region) case meta.Zonal: return fmt.Sprintf("zones/%s", k.Zone) case meta.Global: return "global" default: return "unknownScope" } }