From 7cf766f10b03e7f38ff5c784d6935f34a97a9d1c Mon Sep 17 00:00:00 2001 From: Spencer Hance Date: Tue, 9 Apr 2019 17:33:23 -0700 Subject: [PATCH] Add generator for composite types and generate BackendService --- hack/update-codegen.sh | 3 + pkg/composite/composite.go | 303 ++++++++++---------- pkg/composite/composite_test.go | 261 ++++++++--------- pkg/composite/gen/main.go | 483 ++++++++++++++++++++++++++++++++ pkg/composite/meta/meta.go | 245 ++++++++++++++++ 5 files changed, 1018 insertions(+), 277 deletions(-) create mode 100644 pkg/composite/gen/main.go create mode 100644 pkg/composite/meta/meta.go diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 31bc74ae7f..b92e2d0cff 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -22,6 +22,9 @@ SCRIPT_ROOT=$(dirname ${BASH_SOURCE})/.. CODEGEN_PKG=${CODEGEN_PKG:-$(cd ${SCRIPT_ROOT}; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)} OPENAPI_PKG=${GOPATH}/src/k8s.io/kube-openapi +echo "Generating composite types" +go run ${SCRIPT_ROOT}/pkg/composite/gen/main.go + ${CODEGEN_PKG}/generate-groups.sh \ "deepcopy,client,informer,lister" \ k8s.io/ingress-gce/pkg/backendconfig/client k8s.io/ingress-gce/pkg/apis \ diff --git a/pkg/composite/composite.go b/pkg/composite/composite.go index 2c895308bf..3bc433c999 100644 --- a/pkg/composite/composite.go +++ b/pkg/composite/composite.go @@ -1,5 +1,5 @@ /* -Copyright 2018 The Kubernetes Authors. +Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,6 +13,8 @@ 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. */ +// This file was generated by "go run gen/main.go". Do not edit directly. +// directly. package composite @@ -20,110 +22,39 @@ import ( "encoding/json" "fmt" - "k8s.io/klog" - "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta" computealpha "google.golang.org/api/compute/v0.alpha" computebeta "google.golang.org/api/compute/v0.beta" compute "google.golang.org/api/compute/v1" "google.golang.org/api/googleapi" + "k8s.io/klog" "k8s.io/kubernetes/pkg/cloudprovider/providers/gce" ) -// TODO(rramkumar): All code in this file should ideally be generated. - -func CreateBackendService(be *BackendService, cloud *gce.Cloud) error { - switch be.Version { - case meta.VersionAlpha: - alpha, err := be.toAlpha() - if err != nil { - return err - } - klog.V(3).Infof("Creating alpha backend service %v", alpha.Name) - return cloud.CreateAlphaGlobalBackendService(alpha) - case meta.VersionBeta: - beta, err := be.toBeta() - if err != nil { - return err - } - klog.V(3).Infof("Creating beta backend service %v", beta.Name) - return cloud.CreateBetaGlobalBackendService(beta) - default: - ga, err := be.toGA() - if err != nil { - return err - } - klog.V(3).Infof("Creating ga backend service %v", ga.Name) - return cloud.CreateGlobalBackendService(ga) - } -} - -func UpdateBackendService(be *BackendService, cloud *gce.Cloud) error { - switch be.Version { - case meta.VersionAlpha: - alpha, err := be.toAlpha() - if err != nil { - return err - } - klog.V(3).Infof("Updating alpha backend service %v", alpha.Name) - return cloud.UpdateAlphaGlobalBackendService(alpha) - case meta.VersionBeta: - beta, err := be.toBeta() - if err != nil { - return err - } - klog.V(3).Infof("Updating beta backend service %v", beta.Name) - return cloud.UpdateBetaGlobalBackendService(beta) - default: - ga, err := be.toGA() - if err != nil { - return err - } - klog.V(3).Infof("Updating ga backend service %v", ga.Name) - return cloud.UpdateGlobalBackendService(ga) - } -} - -func GetBackendService(name string, version meta.Version, cloud *gce.Cloud) (*BackendService, error) { - var gceObj interface{} - var err error - switch version { - case meta.VersionAlpha: - gceObj, err = cloud.GetAlphaGlobalBackendService(name) - case meta.VersionBeta: - gceObj, err = cloud.GetBetaGlobalBackendService(name) - default: - gceObj, err = cloud.GetGlobalBackendService(name) - } - if err != nil { - return nil, err - } - return toBackendService(gceObj) -} - -// toBackendService converts a compute alpha, beta or GA -// BackendService into our composite type. -func toBackendService(obj interface{}) (*BackendService, error) { - be := &BackendService{} - bytes, err := json.Marshal(obj) - if err != nil { - return nil, fmt.Errorf("could not marshal object %+v to JSON: %v", obj, err) - } - err = json.Unmarshal(bytes, be) - if err != nil { - return nil, fmt.Errorf("error unmarshalling to BackendService: %v", err) - } - return be, nil +// Backend is a composite type wrapping the Alpha, Beta, and GA methods for its GCE equivalent +type Backend struct { + BalancingMode string `json:"balancingMode,omitempty"` + CapacityScaler float64 `json:"capacityScaler,omitempty"` + Description string `json:"description,omitempty"` + Failover bool `json:"failover,omitempty"` + Group string `json:"group,omitempty"` + MaxConnections int64 `json:"maxConnections,omitempty"` + MaxConnectionsPerEndpoint int64 `json:"maxConnectionsPerEndpoint,omitempty"` + MaxConnectionsPerInstance int64 `json:"maxConnectionsPerInstance,omitempty"` + MaxRate int64 `json:"maxRate,omitempty"` + MaxRatePerEndpoint float64 `json:"maxRatePerEndpoint,omitempty"` + MaxRatePerInstance float64 `json:"maxRatePerInstance,omitempty"` + MaxUtilization float64 `json:"maxUtilization,omitempty"` + ForceSendFields []string `json:"-"` + NullFields []string `json:"-"` } -// BackendService is a composite type which embeds the -// structure of all the compute alpha, beta and GA Backend Service. +// BackendService is a composite type wrapping the Alpha, Beta, and GA methods for its GCE equivalent type BackendService struct { // Version keeps track of the intended compute version for this BackendService. // Note that the compute API's do not contain this field. It is for our // own bookkeeping purposes. - Version meta.Version `json:"-"` - + Version meta.Version `json:"-"` AffinityCookieTtlSec int64 `json:"affinityCookieTtlSec,omitempty"` AppEngineBackend *BackendServiceAppEngineBackend `json:"appEngineBackend,omitempty"` Backends []*Backend `json:"backends,omitempty"` @@ -155,33 +86,53 @@ type BackendService struct { NullFields []string `json:"-"` } -type Backend struct { - BalancingMode string `json:"balancingMode,omitempty"` - CapacityScaler float64 `json:"capacityScaler,omitempty"` - Description string `json:"description,omitempty"` - Failover bool `json:"failover,omitempty"` - Group string `json:"group,omitempty"` - MaxConnections int64 `json:"maxConnections,omitempty"` - MaxConnectionsPerEndpoint int64 `json:"maxConnectionsPerEndpoint,omitempty"` - MaxConnectionsPerInstance int64 `json:"maxConnectionsPerInstance,omitempty"` - MaxRate int64 `json:"maxRate,omitempty"` - MaxRatePerEndpoint float64 `json:"maxRatePerEndpoint,omitempty"` - MaxRatePerInstance float64 `json:"maxRatePerInstance,omitempty"` - MaxUtilization float64 `json:"maxUtilization,omitempty"` - ForceSendFields []string `json:"-"` - NullFields []string `json:"-"` +// BackendServiceAppEngineBackend is a composite type wrapping the Alpha, Beta, and GA methods for its GCE equivalent +type BackendServiceAppEngineBackend struct { + AppEngineService string `json:"appEngineService,omitempty"` + TargetProject string `json:"targetProject,omitempty"` + Version string `json:"version,omitempty"` + ForceSendFields []string `json:"-"` + NullFields []string `json:"-"` +} + +// BackendServiceCdnPolicy is a composite type wrapping the Alpha, Beta, and GA methods for its GCE equivalent +type BackendServiceCdnPolicy struct { + CacheKeyPolicy *CacheKeyPolicy `json:"cacheKeyPolicy,omitempty"` + SignedUrlCacheMaxAgeSec int64 `json:"signedUrlCacheMaxAgeSec,omitempty,string"` + SignedUrlKeyNames []string `json:"signedUrlKeyNames,omitempty"` + ForceSendFields []string `json:"-"` + NullFields []string `json:"-"` +} + +// BackendServiceCloudFunctionBackend is a composite type wrapping the Alpha, Beta, and GA methods for its GCE equivalent +type BackendServiceCloudFunctionBackend struct { + FunctionName string `json:"functionName,omitempty"` + TargetProject string `json:"targetProject,omitempty"` + ForceSendFields []string `json:"-"` + NullFields []string `json:"-"` } +// BackendServiceFailoverPolicy is a composite type wrapping the Alpha, Beta, and GA methods for its GCE equivalent +type BackendServiceFailoverPolicy struct { + DisableConnectionDrainOnFailover bool `json:"disableConnectionDrainOnFailover,omitempty"` + DropTrafficIfUnhealthy bool `json:"dropTrafficIfUnhealthy,omitempty"` + FailoverRatio float64 `json:"failoverRatio,omitempty"` + ForceSendFields []string `json:"-"` + NullFields []string `json:"-"` +} + +// BackendServiceIAP is a composite type wrapping the Alpha, Beta, and GA methods for its GCE equivalent type BackendServiceIAP struct { Enabled bool `json:"enabled,omitempty"` Oauth2ClientId string `json:"oauth2ClientId,omitempty"` + Oauth2ClientInfo *BackendServiceIAPOAuth2ClientInfo `json:"oauth2ClientInfo,omitempty"` Oauth2ClientSecret string `json:"oauth2ClientSecret,omitempty"` Oauth2ClientSecretSha256 string `json:"oauth2ClientSecretSha256,omitempty"` ForceSendFields []string `json:"-"` NullFields []string `json:"-"` - Oauth2ClientInfo *BackendServiceIAPOAuth2ClientInfo `json:"oauth2ClientInfo,omitempty"` } +// BackendServiceIAPOAuth2ClientInfo is a composite type wrapping the Alpha, Beta, and GA methods for its GCE equivalent type BackendServiceIAPOAuth2ClientInfo struct { ApplicationName string `json:"applicationName,omitempty"` ClientName string `json:"clientName,omitempty"` @@ -190,14 +141,7 @@ type BackendServiceIAPOAuth2ClientInfo struct { NullFields []string `json:"-"` } -type BackendServiceCdnPolicy struct { - CacheKeyPolicy *CacheKeyPolicy `json:"cacheKeyPolicy,omitempty"` - SignedUrlCacheMaxAgeSec int64 `json:"signedUrlCacheMaxAgeSec,omitempty,string"` - SignedUrlKeyNames []string `json:"signedUrlKeyNames,omitempty"` - ForceSendFields []string `json:"-"` - NullFields []string `json:"-"` -} - +// CacheKeyPolicy is a composite type wrapping the Alpha, Beta, and GA methods for its GCE equivalent type CacheKeyPolicy struct { IncludeHost bool `json:"includeHost,omitempty"` IncludeProtocol bool `json:"includeProtocol,omitempty"` @@ -208,39 +152,101 @@ type CacheKeyPolicy struct { NullFields []string `json:"-"` } -type BackendServiceFailoverPolicy struct { - DisableConnectionDrainOnFailover bool `json:"disableConnectionDrainOnFailover,omitempty"` - DropTrafficIfUnhealthy bool `json:"dropTrafficIfUnhealthy,omitempty"` - FailoverRatio float64 `json:"failoverRatio,omitempty"` - ForceSendFields []string `json:"-"` - NullFields []string `json:"-"` -} - -type BackendServiceCloudFunctionBackend struct { - FunctionName string `json:"functionName,omitempty"` - TargetProject string `json:"targetProject,omitempty"` - ForceSendFields []string `json:"-"` - NullFields []string `json:"-"` -} - +// ConnectionDraining is a composite type wrapping the Alpha, Beta, and GA methods for its GCE equivalent type ConnectionDraining struct { DrainingTimeoutSec int64 `json:"drainingTimeoutSec,omitempty"` ForceSendFields []string `json:"-"` NullFields []string `json:"-"` } -type BackendServiceAppEngineBackend struct { - AppEngineService string `json:"appEngineService,omitempty"` - TargetProject string `json:"targetProject,omitempty"` - Version string `json:"version,omitempty"` - ForceSendFields []string `json:"-"` - NullFields []string `json:"-"` +func CreateBackendService(backendService *BackendService, cloud *gce.Cloud) error { + switch backendService.Version { + case meta.VersionAlpha: + alpha, err := backendService.toAlpha() + if err != nil { + return err + } + klog.V(3).Infof("Creating alpha BackendService %v", alpha.Name) + return cloud.CreateAlphaGlobalBackendService(alpha) + case meta.VersionBeta: + beta, err := backendService.toBeta() + if err != nil { + return err + } + klog.V(3).Infof("Creating beta BackendService %v", beta.Name) + return cloud.CreateBetaGlobalBackendService(beta) + default: + ga, err := backendService.toGA() + if err != nil { + return err + } + klog.V(3).Infof("Creating ga BackendService %v", ga.Name) + return cloud.CreateGlobalBackendService(ga) + } +} + +func UpdateBackendService(backendService *BackendService, cloud *gce.Cloud) error { + switch backendService.Version { + case meta.VersionAlpha: + alpha, err := backendService.toAlpha() + if err != nil { + return err + } + klog.V(3).Infof("Updating alpha BackendService %v", alpha.Name) + return cloud.UpdateAlphaGlobalBackendService(alpha) + case meta.VersionBeta: + beta, err := backendService.toBeta() + if err != nil { + return err + } + klog.V(3).Infof("Updating beta BackendService %v", beta.Name) + return cloud.UpdateBetaGlobalBackendService(beta) + default: + ga, err := backendService.toGA() + if err != nil { + return err + } + klog.V(3).Infof("Updating ga BackendService %v", ga.Name) + return cloud.UpdateGlobalBackendService(ga) + } +} + +func GetBackendService(name string, version meta.Version, cloud *gce.Cloud) (*BackendService, error) { + var gceObj interface{} + var err error + switch version { + case meta.VersionAlpha: + gceObj, err = cloud.GetAlphaGlobalBackendService(name) + case meta.VersionBeta: + gceObj, err = cloud.GetBetaGlobalBackendService(name) + default: + gceObj, err = cloud.GetGlobalBackendService(name) + } + if err != nil { + return nil, err + } + return toBackendService(gceObj) +} + +// toBackendService converts a compute alpha, beta or GA +// BackendService into our composite type. +func toBackendService(obj interface{}) (*BackendService, error) { + be := &BackendService{} + bytes, err := json.Marshal(obj) + if err != nil { + return nil, fmt.Errorf("could not marshal object %+v to JSON: %v", obj, err) + } + err = json.Unmarshal(bytes, be) + if err != nil { + return nil, fmt.Errorf("error unmarshalling to BackendService: %v", err) + } + return be, nil } // toAlpha converts our composite type into an alpha type. // This alpha type can be used in GCE API calls. -func (be *BackendService) toAlpha() (*computealpha.BackendService, error) { - bytes, err := json.Marshal(be) +func (backendService *BackendService) toAlpha() (*computealpha.BackendService, error) { + bytes, err := json.Marshal(backendService) if err != nil { return nil, fmt.Errorf("error marshalling BackendService to JSON: %v", err) } @@ -256,13 +262,14 @@ func (be *BackendService) toAlpha() (*computealpha.BackendService, error) { if alpha.Iap != nil { alpha.Iap.ForceSendFields = []string{"Enabled", "Oauth2ClientId", "Oauth2ClientSecret"} } + return alpha, nil } -// toBeta converts our composite type into an beta type. -// This beta type can be used in GCE API calls. -func (be *BackendService) toBeta() (*computebeta.BackendService, error) { - bytes, err := json.Marshal(be) +// toBeta converts our composite type into an alpha type. +// This alpha type can be used in GCE API calls. +func (backendService *BackendService) toBeta() (*computebeta.BackendService, error) { + bytes, err := json.Marshal(backendService) if err != nil { return nil, fmt.Errorf("error marshalling BackendService to JSON: %v", err) } @@ -278,20 +285,21 @@ func (be *BackendService) toBeta() (*computebeta.BackendService, error) { if beta.Iap != nil { beta.Iap.ForceSendFields = []string{"Enabled", "Oauth2ClientId", "Oauth2ClientSecret"} } + return beta, nil } -// toGA converts our composite type into a GA type. -// This GA type can be used in GCE API calls. -func (be *BackendService) toGA() (*compute.BackendService, error) { - bytes, err := json.Marshal(be) +// toGA converts our composite type into an alpha type. +// This alpha type can be used in GCE API calls. +func (backendService *BackendService) toGA() (*compute.BackendService, error) { + bytes, err := json.Marshal(backendService) if err != nil { return nil, fmt.Errorf("error marshalling BackendService to JSON: %v", err) } ga := &compute.BackendService{} err = json.Unmarshal(bytes, ga) if err != nil { - return nil, fmt.Errorf("error unmarshalling BackendService JSON to compute GA type: %v", err) + return nil, fmt.Errorf("error unmarshalling BackendService JSON to compute ga type: %v", err) } // Set force send fields. This is a temporary hack. if ga.CdnPolicy != nil && ga.CdnPolicy.CacheKeyPolicy != nil { @@ -300,5 +308,6 @@ func (be *BackendService) toGA() (*compute.BackendService, error) { if ga.Iap != nil { ga.Iap.ForceSendFields = []string{"Enabled", "Oauth2ClientId", "Oauth2ClientSecret"} } + return ga, nil } diff --git a/pkg/composite/composite_test.go b/pkg/composite/composite_test.go index 975bbe661e..21c4c17fb7 100644 --- a/pkg/composite/composite_test.go +++ b/pkg/composite/composite_test.go @@ -1,5 +1,5 @@ /* -Copyright 2018 The Kubernetes Authors. +Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,6 +13,8 @@ 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. */ +// This file was generated by "go run gen/main.go". Do not edit directly. +// directly. package composite @@ -21,14 +23,56 @@ import ( "reflect" "testing" - "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta" "github.com/kr/pretty" computealpha "google.golang.org/api/compute/v0.alpha" + computebeta "google.golang.org/api/compute/v0.beta" compute "google.golang.org/api/compute/v1" ) -// TODO(rramkumar): All code in this file should ideally be generated. +// compareFields verifies that two fields in a struct have the same relevant metadata. +// Note: This comparison ignores field offset, index, and pkg path, all of which don't matter. +func compareFields(s1, s2 reflect.StructField) error { + if s1.Name != s2.Name { + return fmt.Errorf("field %s name = %q, want %q", s1.Name, s1.Name, s2.Name) + } + if s1.Tag != s2.Tag { + return fmt.Errorf("field %s tag = %q, want %q", s1.Name, s1.Tag, s2.Tag) + } + if s1.Type.Name() != s2.Type.Name() { + return fmt.Errorf("field %s type = %q, want %q", s1.Name, s1.Type.Name(), s2.Type.Name()) + } + return nil +} + +// typeEquality is a generic function that checks type equality. +func typeEquality(t1, t2 reflect.Type) error { + t1Fields, t2Fields := make(map[string]bool), make(map[string]bool) + for i := 0; i < t1.NumField(); i++ { + t1Fields[t1.Field(i).Name] = true + } + for i := 0; i < t2.NumField(); i++ { + t2Fields[t2.Field(i).Name] = true + } + if !reflect.DeepEqual(t1Fields, t2Fields) { + return fmt.Errorf("type = %+v, want %+v", t1Fields, t2Fields) + } + for n := range t1Fields { + f1, _ := t1.FieldByName(n) + f2, _ := t2.FieldByName(n) + if err := compareFields(f1, f2); err != nil { + return err + } + } + return nil +} +func TestBackend(t *testing.T) { + compositeType := reflect.TypeOf(Backend{}) + alphaType := reflect.TypeOf(computealpha.Backend{}) + if err := typeEquality(compositeType, alphaType); err != nil { + t.Fatal(err) + } +} func TestBackendService(t *testing.T) { // Use reflection to verify that our composite type contains all the // same fields as the alpha type. @@ -40,33 +84,85 @@ func TestBackendService(t *testing.T) { if compositeTypeNumFields != alphaType.NumField() { t.Fatalf("%v should contain %v fields. Got %v", alphaType.Name(), alphaType.NumField(), compositeTypeNumFields) } - // Start loop at 1 to ignore the composite type's Version field. + + // Compare all the fields by doing a lookup since we can't guarantee that they'll be in the same order for i := 1; i < compositeType.NumField(); i++ { - if err := compareFields(compositeType.Field(i), alphaType.Field(i-1)); err != nil { + lookupField, found := alphaType.FieldByName(compositeType.Field(i).Name) + if !found { + t.Fatal(fmt.Errorf("Field %v not present in alpha type %v", compositeType.Field(i), alphaType)) + } + if err := compareFields(compositeType.Field(i), lookupField); err != nil { t.Fatal(err) } } } -func TestBackend(t *testing.T) { - compositeType := reflect.TypeOf(Backend{}) - alphaType := reflect.TypeOf(computealpha.Backend{}) - if err := typeEquality(compositeType, alphaType); err != nil { - t.Fatal(err) +func TestToBackendService(t *testing.T) { + testCases := []struct { + input interface{} + expected *BackendService + }{ + { + computealpha.BackendService{}, + &BackendService{}, + }, + { + computebeta.BackendService{}, + &BackendService{}, + }, + { + compute.BackendService{}, + &BackendService{}, + }, + } + for _, testCase := range testCases { + result, _ := toBackendService(testCase.input) + if !reflect.DeepEqual(result, testCase.expected) { + t.Fatalf("toBackendService(input) = \ninput = %s\n%s\nwant = \n%s", pretty.Sprint(testCase.input), pretty.Sprint(result), pretty.Sprint(testCase.expected)) + } } } -func TestBackendServiceIAP(t *testing.T) { - compositeType := reflect.TypeOf(BackendServiceIAP{}) - alphaType := reflect.TypeOf(computealpha.BackendServiceIAP{}) - if err := typeEquality(compositeType, alphaType); err != nil { - t.Fatal(err) +func TestBackendServiceToAlpha(t *testing.T) { + composite := BackendService{} + expected := &computealpha.BackendService{} + result, err := composite.toAlpha() + if err != nil { + t.Fatalf("BackendService.toAlpha() error: %v", err) + } + + if !reflect.DeepEqual(result, expected) { + t.Fatalf("BackendService.toAlpha() = \ninput = %s\n%s\nwant = \n%s", pretty.Sprint(composite), pretty.Sprint(result), pretty.Sprint(expected)) } } +func TestBackendServiceToBeta(t *testing.T) { + composite := BackendService{} + expected := &computebeta.BackendService{} + result, err := composite.toBeta() + if err != nil { + t.Fatalf("BackendService.toBeta() error: %v", err) + } -func TestBackendServiceIAPOAuth2ClientInfo(t *testing.T) { - compositeType := reflect.TypeOf(BackendServiceIAPOAuth2ClientInfo{}) - alphaType := reflect.TypeOf(computealpha.BackendServiceIAPOAuth2ClientInfo{}) + if !reflect.DeepEqual(result, expected) { + t.Fatalf("BackendService.toBeta() = \ninput = %s\n%s\nwant = \n%s", pretty.Sprint(composite), pretty.Sprint(result), pretty.Sprint(expected)) + } +} +func TestBackendServiceToGA(t *testing.T) { + composite := BackendService{} + expected := &compute.BackendService{} + result, err := composite.toGA() + if err != nil { + t.Fatalf("BackendService.toGA() error: %v", err) + } + + if !reflect.DeepEqual(result, expected) { + t.Fatalf("BackendService.toGA() = \ninput = %s\n%s\nwant = \n%s", pretty.Sprint(composite), pretty.Sprint(result), pretty.Sprint(expected)) + } +} + +func TestBackendServiceAppEngineBackend(t *testing.T) { + compositeType := reflect.TypeOf(BackendServiceAppEngineBackend{}) + alphaType := reflect.TypeOf(computealpha.BackendServiceAppEngineBackend{}) if err := typeEquality(compositeType, alphaType); err != nil { t.Fatal(err) } @@ -80,9 +176,9 @@ func TestBackendServiceCdnPolicy(t *testing.T) { } } -func TestCacheKeyPolicy(t *testing.T) { - compositeType := reflect.TypeOf(CacheKeyPolicy{}) - alphaType := reflect.TypeOf(computealpha.CacheKeyPolicy{}) +func TestBackendServiceCloudFunctionBackend(t *testing.T) { + compositeType := reflect.TypeOf(BackendServiceCloudFunctionBackend{}) + alphaType := reflect.TypeOf(computealpha.BackendServiceCloudFunctionBackend{}) if err := typeEquality(compositeType, alphaType); err != nil { t.Fatal(err) } @@ -96,129 +192,34 @@ func TestBackendServiceFailoverPolicy(t *testing.T) { } } -func TestBackendServiceCloudFunctionBackend(t *testing.T) { - compositeType := reflect.TypeOf(BackendServiceCloudFunctionBackend{}) - alphaType := reflect.TypeOf(computealpha.BackendServiceCloudFunctionBackend{}) +func TestBackendServiceIAP(t *testing.T) { + compositeType := reflect.TypeOf(BackendServiceIAP{}) + alphaType := reflect.TypeOf(computealpha.BackendServiceIAP{}) if err := typeEquality(compositeType, alphaType); err != nil { t.Fatal(err) } } -func TestConnectionDraining(t *testing.T) { - compositeType := reflect.TypeOf(ConnectionDraining{}) - alphaType := reflect.TypeOf(computealpha.ConnectionDraining{}) +func TestBackendServiceIAPOAuth2ClientInfo(t *testing.T) { + compositeType := reflect.TypeOf(BackendServiceIAPOAuth2ClientInfo{}) + alphaType := reflect.TypeOf(computealpha.BackendServiceIAPOAuth2ClientInfo{}) if err := typeEquality(compositeType, alphaType); err != nil { t.Fatal(err) } } -func TestBackendServiceAppEngineBackend(t *testing.T) { - compositeType := reflect.TypeOf(BackendServiceAppEngineBackend{}) - alphaType := reflect.TypeOf(computealpha.BackendServiceAppEngineBackend{}) +func TestCacheKeyPolicy(t *testing.T) { + compositeType := reflect.TypeOf(CacheKeyPolicy{}) + alphaType := reflect.TypeOf(computealpha.CacheKeyPolicy{}) if err := typeEquality(compositeType, alphaType); err != nil { t.Fatal(err) } } -func TestToBackendService(t *testing.T) { - testCases := []struct { - input interface{} - expected *BackendService - }{ - { - computealpha.BackendService{Protocol: "HTTP2"}, - &BackendService{Protocol: "HTTP2"}, - }, - { - compute.BackendService{Protocol: "HTTP"}, - &BackendService{Protocol: "HTTP"}, - }, - } - for _, testCase := range testCases { - result, _ := toBackendService(testCase.input) - if !reflect.DeepEqual(result, testCase.expected) { - t.Fatalf("toBackendService(input) = \ninput = %s\n%s\nwant = \n%s", pretty.Sprint(testCase.input), pretty.Sprint(result), pretty.Sprint(testCase.expected)) - } - } -} - -func TestToAlpha(t *testing.T) { - composite := BackendService{ - Version: meta.VersionAlpha, - Protocol: "HTTP2", - } - alpha, err := composite.toAlpha() - if err != nil { - t.Fatalf("composite.toAlpha() =_, %v; want _, nil", err) - } - // Verify that a known alpha field value is preserved. - if alpha.Protocol != "HTTP2" { - t.Errorf("alpha.Protocol = %q, want HTTP2", alpha.Protocol) - } -} - -func TestToBeta(t *testing.T) { - composite := BackendService{ - Version: meta.VersionGA, - SecurityPolicy: "foo", - } - beta, err := composite.toBeta() - if err != nil { - t.Fatalf("composite.toBeta() =_, %v; want _, nil", err) - } - // Verify that a known beta field value is preserved. - if beta.SecurityPolicy != "foo" { - t.Errorf("beta.SecurityPolicy = %q, want foo", beta.SecurityPolicy) - } -} - -func TestToGA(t *testing.T) { - composite := BackendService{ - Version: meta.VersionGA, - Protocol: "HTTP", - } - ga, err := composite.toGA() - if err != nil { - t.Fatalf("composite.toGA() =_, %v; want _, nil", err) - } - if ga.Protocol != "HTTP" { - t.Errorf("ga.Protocol = %q, want HTTP", ga.Protocol) - } -} - -// typeEquality is a generic function that checks type equality. -func typeEquality(t1, t2 reflect.Type) error { - t1Fields, t2Fields := make(map[string]bool), make(map[string]bool) - for i := 0; i < t1.NumField(); i++ { - t1Fields[t1.Field(i).Name] = true - } - for i := 0; i < t2.NumField(); i++ { - t2Fields[t2.Field(i).Name] = true - } - if !reflect.DeepEqual(t1Fields, t2Fields) { - return fmt.Errorf("type = %+v, want %+v", t1Fields, t2Fields) - } - for n := range t1Fields { - f1, _ := t1.FieldByName(n) - f2, _ := t2.FieldByName(n) - if err := compareFields(f1, f2); err != nil { - return err - } - } - return nil -} - -// compareFields verifies that two fields in a struct have the same relevant metadata. -// Note: This comparison ignores field offset, index, and pkg path, all of which don't matter. -func compareFields(s1, s2 reflect.StructField) error { - if s1.Name != s2.Name { - return fmt.Errorf("field %s name = %q, want %q", s1.Name, s1.Name, s2.Name) - } - if s1.Tag != s2.Tag { - return fmt.Errorf("field %s tag = %q, want %q", s1.Name, s1.Tag, s2.Tag) - } - if s1.Type.Name() != s2.Type.Name() { - return fmt.Errorf("field %s type = %q, want %q", s1.Name, s1.Type.Name(), s2.Type.Name()) +func TestConnectionDraining(t *testing.T) { + compositeType := reflect.TypeOf(ConnectionDraining{}) + alphaType := reflect.TypeOf(computealpha.ConnectionDraining{}) + if err := typeEquality(compositeType, alphaType); err != nil { + t.Fatal(err) } - return nil } diff --git a/pkg/composite/gen/main.go b/pkg/composite/gen/main.go new file mode 100644 index 0000000000..aa4bcd5dba --- /dev/null +++ b/pkg/composite/gen/main.go @@ -0,0 +1,483 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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. +*/ + +// Generator for GCE compute wrapper code. You must regenerate the code after +// modifying this file: +// +// $ ./hack/update_codegen.sh + +package main + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "strings" + "text/template" + "time" + + compositemeta "k8s.io/ingress-gce/pkg/composite/meta" +) + +const ( + gofmt = "gofmt" +) + +// gofmtContent runs "gofmt" on the given contents. +func gofmtContent(r io.Reader) string { + cmd := exec.Command(gofmt, "-s") + out := &bytes.Buffer{} + cmd.Stdin = r + cmd.Stdout = out + cmdErr := &bytes.Buffer{} + cmd.Stderr = cmdErr + + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, cmdErr.String()) + panic(err) + } + return out.String() +} + +func genHeader(wr io.Writer) { + const text = `/* +Copyright {{.Year}} The Kubernetes Authors. + +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. +*/ +// This file was generated by "go run gen/main.go". Do not edit directly. +// directly. + +package composite +import ( + "encoding/json" + "fmt" + + "k8s.io/klog" + computealpha "google.golang.org/api/compute/v0.alpha" + computebeta "google.golang.org/api/compute/v0.beta" + compute "google.golang.org/api/compute/v1" + "google.golang.org/api/googleapi" + "k8s.io/kubernetes/pkg/cloudprovider/providers/gce" + "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta" +) +` + tmpl := template.Must(template.New("header").Parse(text)) + values := map[string]string{ + "Year": fmt.Sprintf("%v", time.Now().Year()), + } + if err := tmpl.Execute(wr, values); err != nil { + panic(err) + } + + fmt.Fprintf(wr, "\n\n") +} + +func genTestHeader(wr io.Writer) { + const text = `/* +Copyright {{.Year}} The Kubernetes Authors. + +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. +*/ +// This file was generated by "go run gen/main.go". Do not edit directly. +// directly. + +package composite +import ( + "fmt" + "reflect" + "testing" + + "github.com/kr/pretty" + computealpha "google.golang.org/api/compute/v0.alpha" + computebeta "google.golang.org/api/compute/v0.beta" + compute "google.golang.org/api/compute/v1" +) +` + tmpl := template.Must(template.New("testHeader").Parse(text)) + values := map[string]string{ + "Year": fmt.Sprintf("%v", time.Now().Year()), + } + if err := tmpl.Execute(wr, values); err != nil { + panic(err) + } + + fmt.Fprintf(wr, "\n\n") +} + +// genTypes() generates all of the composite structs. +func genTypes(wr io.Writer) { + const text = ` +{{ $backtick := "` + "`" + `" }} +{{- range .All}} + // {{.Name}} is a composite type wrapping the Alpha, Beta, and GA methods for its GCE equivalent + type {{.Name}} struct { + {{- if .IsMainService}} + // Version keeps track of the intended compute version for this {{.Name}}. + // Note that the compute API's do not contain this field. It is for our + // own bookkeeping purposes. + Version meta.Version {{$backtick}}json:"-"{{$backtick}} + {{- end}} + + {{- range .Fields}} + {{- if eq .Name "Id"}} + {{.Name}} {{.GoType}} {{$backtick}}json:"{{.JsonName}},omitempty,string"{{$backtick}} + {{- else if .JsonStringOverride}} + {{.Name}} {{.GoType}} {{$backtick}}json:"{{.JsonName}},omitempty,string"{{$backtick}} + {{- else}} + {{.Name}} {{.GoType}} {{$backtick}}json:"{{.JsonName}},omitempty"{{$backtick}} + {{- end}} + {{- end}} + {{- if .IsMainService}} + googleapi.ServerResponse {{$backtick}}json:"-"{{$backtick}} + {{- end}} + ForceSendFields []string {{$backtick}}json:"-"{{$backtick}} + NullFields []string {{$backtick}}json:"-"{{$backtick}} +} +{{- end}} +` + data := struct { + All []compositemeta.ApiService + }{compositemeta.AllApiServices} + + tmpl := template.Must(template.New("types").Parse(text)) + if err := tmpl.Execute(wr, data); err != nil { + panic(err) + } +} + +// genFuncs() generates helper methods attached to composite structs. +// TODO: (shance) generated CRUD functions should take a meta.Key object to allow easier use of global and regional resources +func genFuncs(wr io.Writer) { + const text = ` +{{$All := .All}} +{{$Versions := .Versions}} + +{{range $type := $All}} +{{if .IsMainService}} + func Create{{.Name}}({{.VarName}} *{{.Name}}, cloud *gce.Cloud) error { + switch {{.VarName}}.Version { + case meta.VersionAlpha: + alpha, err := {{.VarName}}.toAlpha() + if err != nil { + return err + } + klog.V(3).Infof("Creating alpha {{.Name}} %v", alpha.Name) + return cloud.CreateAlphaGlobal{{.Name}}(alpha) + case meta.VersionBeta: + beta, err := {{.VarName}}.toBeta() + if err != nil { + return err + } + klog.V(3).Infof("Creating beta {{.Name}} %v", beta.Name) + return cloud.CreateBetaGlobal{{.Name}}(beta) + default: + ga, err := {{.VarName}}.toGA() + if err != nil { + return err + } + klog.V(3).Infof("Creating ga {{.Name}} %v", ga.Name) + return cloud.CreateGlobal{{.Name}}(ga) + } +} + +{{if .HasUpdate}} +func Update{{.Name}}({{.VarName}} *{{.Name}}, cloud *gce.Cloud) error { + switch {{.VarName}}.Version { + case meta.VersionAlpha: + alpha, err := {{.VarName}}.toAlpha() + if err != nil { + return err + } + klog.V(3).Infof("Updating alpha {{.Name}} %v", alpha.Name) + return cloud.UpdateAlphaGlobal{{.Name}}(alpha) + case meta.VersionBeta: + beta, err := {{.VarName}}.toBeta() + if err != nil { + return err + } + klog.V(3).Infof("Updating beta {{.Name}} %v", beta.Name) + return cloud.UpdateBetaGlobal{{.Name}}(beta) + default: + ga, err := {{.VarName}}.toGA() + if err != nil { + return err + } + klog.V(3).Infof("Updating ga {{.Name}} %v", ga.Name) + return cloud.UpdateGlobal{{.Name}}(ga) + } +} +{{- end}} + +func Get{{.Name}}(name string, version meta.Version, cloud *gce.Cloud) (*{{.Name}}, error) { + var gceObj interface{} + var err error + switch version { + case meta.VersionAlpha: + gceObj, err = cloud.GetAlphaGlobal{{.Name}}(name) + case meta.VersionBeta: + gceObj, err = cloud.GetBetaGlobal{{.Name}}(name) + default: + gceObj, err = cloud.GetGlobal{{.Name}}(name) + } + if err != nil { + return nil, err + } + return to{{.Name}}(gceObj) +} + +// to{{.Name}} converts a compute alpha, beta or GA +// {{.Name}} into our composite type. +func to{{.Name}}(obj interface{}) (*{{.Name}}, error) { + be := &{{.Name}}{} + bytes, err := json.Marshal(obj) + if err != nil { + return nil, fmt.Errorf("could not marshal object %+v to JSON: %v", obj, err) + } + err = json.Unmarshal(bytes, be) + if err != nil { + return nil, fmt.Errorf("error unmarshalling to {{.Name}}: %v", err) + } + return be, nil +} + +{{- range $version, $extension := $.Versions}} +{{$lower := $version | ToLower}} +// to{{$version}} converts our composite type into an alpha type. +// This alpha type can be used in GCE API calls. +func ({{$type.VarName}} *{{$type.Name}}) to{{$version}}() (*compute{{$extension}}.{{$type.Name}}, error) { + bytes, err := json.Marshal({{$type.VarName}}) + if err != nil { + return nil, fmt.Errorf("error marshalling {{$type.Name}} to JSON: %v", err) + } + {{$version | ToLower}} := &compute{{$extension}}.{{$type.Name}}{} + err = json.Unmarshal(bytes, {{$lower}}) + if err != nil { + return nil, fmt.Errorf("error unmarshalling {{$type.Name}} JSON to compute {{$lower}} type: %v", err) + } + + {{- if eq $type.Name "BackendService"}} + // Set force send fields. This is a temporary hack. + if {{$lower}}.CdnPolicy != nil && {{$lower}}.CdnPolicy.CacheKeyPolicy != nil { + {{$lower}}.CdnPolicy.CacheKeyPolicy.ForceSendFields = []string{"IncludeHost", "IncludeProtocol", "IncludeQueryString", "QueryStringBlacklist", "QueryStringWhitelist"} + } + if {{$lower}}.Iap != nil { + {{$lower}}.Iap.ForceSendFields = []string{"Enabled", "Oauth2ClientId", "Oauth2ClientSecret"} + } + {{- end}} + + return {{$lower}}, nil +} +{{- end}} + + +{{- end}} +{{- end}} +` + data := struct { + All []compositemeta.ApiService + Versions map[string]string + }{compositemeta.AllApiServices, compositemeta.Versions} + + funcMap := template.FuncMap{ + "ToLower": strings.ToLower, + } + + tmpl := template.Must(template.New("funcs").Funcs(funcMap).Parse(text)) + if err := tmpl.Execute(wr, data); err != nil { + panic(err) + } +} + +// genTests() generates all of the tests +func genTests(wr io.Writer) { + const text = ` +// compareFields verifies that two fields in a struct have the same relevant metadata. +// Note: This comparison ignores field offset, index, and pkg path, all of which don't matter. +func compareFields(s1, s2 reflect.StructField) error { + if s1.Name != s2.Name { + return fmt.Errorf("field %s name = %q, want %q", s1.Name, s1.Name, s2.Name) + } + if s1.Tag != s2.Tag { + return fmt.Errorf("field %s tag = %q, want %q", s1.Name, s1.Tag, s2.Tag) + } + if s1.Type.Name() != s2.Type.Name() { + return fmt.Errorf("field %s type = %q, want %q", s1.Name, s1.Type.Name(), s2.Type.Name()) + } + return nil +} + +// typeEquality is a generic function that checks type equality. +func typeEquality(t1, t2 reflect.Type) error { + t1Fields, t2Fields := make(map[string]bool), make(map[string]bool) + for i := 0; i < t1.NumField(); i++ { + t1Fields[t1.Field(i).Name] = true + } + for i := 0; i < t2.NumField(); i++ { + t2Fields[t2.Field(i).Name] = true + } + if !reflect.DeepEqual(t1Fields, t2Fields) { + return fmt.Errorf("type = %+v, want %+v", t1Fields, t2Fields) + } + for n := range t1Fields { + f1, _ := t1.FieldByName(n) + f2, _ := t2.FieldByName(n) + if err := compareFields(f1, f2); err != nil { + return err + } + } + return nil +} + +{{ $All := .All}} +{{range $type := $All}} + {{- if .IsMainService}} + func Test{{.Name}}(t *testing.T) { + // Use reflection to verify that our composite type contains all the + // same fields as the alpha type. + compositeType := reflect.TypeOf({{.Name}}{}) + alphaType := reflect.TypeOf(computealpha.{{.Name}}{}) + + // For the composite type, remove the Version field from consideration + compositeTypeNumFields := compositeType.NumField() - 1 + if compositeTypeNumFields != alphaType.NumField() { + t.Fatalf("%v should contain %v fields. Got %v", alphaType.Name(), alphaType.NumField(), compositeTypeNumFields) + } + + // Compare all the fields by doing a lookup since we can't guarantee that they'll be in the same order + for i := 1; i < compositeType.NumField(); i++ { + lookupField, found := alphaType.FieldByName(compositeType.Field(i).Name) + if !found { + t.Fatal(fmt.Errorf("Field %v not present in alpha type %v", compositeType.Field(i), alphaType)) + } + if err := compareFields(compositeType.Field(i), lookupField); err != nil { + t.Fatal(err) + } + } +} + +func TestTo{{.Name}}(t *testing.T) { + testCases := []struct { + input interface{} + expected *{{.Name}} + }{ + { + computealpha.{{.Name}}{}, + &{{.Name}}{}, + }, + { + computebeta.{{.Name}}{}, + &{{.Name}}{}, + }, + { + compute.{{.Name}}{}, + &{{.Name}}{}, + }, + } + for _, testCase := range testCases { + result, _ := to{{.Name}}(testCase.input) + if !reflect.DeepEqual(result, testCase.expected) { + t.Fatalf("to{{.Name}}(input) = \ninput = %s\n%s\nwant = \n%s", pretty.Sprint(testCase.input), pretty.Sprint(result), pretty.Sprint(testCase.expected)) + } + } +} + +{{range $version, $extension := $.Versions}} +func Test{{$type.Name}}To{{$version}}(t *testing.T) { + composite := {{$type.Name}}{} + expected := &compute{{$extension}}.{{$type.Name}}{} + result, err := composite.to{{$version}}() + if err != nil { + t.Fatalf("{{$type.Name}}.to{{$version}}() error: %v", err) + } + + if !reflect.DeepEqual(result, expected) { + t.Fatalf("{{$type.Name}}.to{{$version}}() = \ninput = %s\n%s\nwant = \n%s", pretty.Sprint(composite), pretty.Sprint(result), pretty.Sprint(expected)) + } +} +{{- end}} + +{{- else}} + +func Test{{.Name}}(t *testing.T) { + compositeType := reflect.TypeOf({{.Name}}{}) + alphaType := reflect.TypeOf(computealpha.{{.Name}}{}) + if err := typeEquality(compositeType, alphaType); err != nil { + t.Fatal(err) + } +} + {{- end}} +{{- end}} +` + data := struct { + All []compositemeta.ApiService + Versions map[string]string + }{compositemeta.AllApiServices, compositemeta.Versions} + + funcMap := template.FuncMap{ + "ToLower": strings.ToLower, + } + + tmpl := template.Must(template.New("tests").Funcs(funcMap).Parse(text)) + if err := tmpl.Execute(wr, data); err != nil { + panic(err) + } +} + +func main() { + out := &bytes.Buffer{} + testOut := &bytes.Buffer{} + + genHeader(out) + genTypes(out) + genFuncs(out) + + genTestHeader(testOut) + genTests(testOut) + + var err error + err = ioutil.WriteFile("./pkg/composite/composite.go", []byte(gofmtContent(out)), 0644) + if err != nil { + panic(err) + } + err = ioutil.WriteFile("./pkg/composite/composite_test.go", []byte(gofmtContent(testOut)), 0644) + if err != nil { + panic(err) + } +} diff --git a/pkg/composite/meta/meta.go b/pkg/composite/meta/meta.go new file mode 100644 index 0000000000..c13618cd20 --- /dev/null +++ b/pkg/composite/meta/meta.go @@ -0,0 +1,245 @@ +/* +Copyright 2019 The Kubernetes Authors. +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 meta + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "k8s.io/apimachinery/pkg/util/sets" + "os" + "sort" + "strings" + "unicode" +) + +const ( + // This assumes that alpha contains a superset of all struct fields + apiFilePath = "./vendor/google.golang.org/api/compute/v0.alpha/compute-api.json" +) + +// MainServices describes all of the API types that we want to define all the helper functions for +// The other types that are discovered as dependencies will simply be wrapped with a composite struct +// The format of the map is ServiceName -> k8s-cloud-provider wrapper name +// TODO: (shance) Add the commented services and remove dependency on first cloud-provider layer +var MainServices = map[string]string{ + "BackendService": "BackendServices", + /* + "ForwardingRule": "ForwardingRules", + "HttpHealthCheck": "HttpHealthChecks", + "HttpsHealthCheck": "HttpsHealthChecks", + "UrlMap": "UrlMaps", + "TargetHttpProxy": "TargetHttpProxies", + "TargetHttpsProxy": "TargetHttpsProxies", + */ +} + +// TODO: (shance) Replace this with data gathered from meta.AllServices +// Services in NoUpdate will not have an Update() method generated for them +var NoUpdate = sets.NewString( + "ForwardingRule", + "TargetHttpProxy", + "TargetHttpsProxy", +) + +var Versions = map[string]string{ + "Alpha": "alpha", + "Beta": "beta", + "GA": "", +} + +// ApiService holds relevant data for generating a composite type + helper methods for a single API service +type ApiService struct { + // Name of the Go struct + Name string + // Name used in the Json tag for marshalling/unmarshalling + JsonName string + // Force JSON tag as string type + JsonStringOverride bool + // Golang type + GoType string + // Name to use when creating an instance of this type + VarName string + // All of the struct fields + Fields []ApiService +} + +// IsMainService() returns true if the service name is in the MainServices map +func (apiService *ApiService) IsMainService() bool { + _, found := MainServices[apiService.Name] + return found +} + +// HasUpdate() returns true if the service name is *not* in the NoUpdate() list +func (apiService *ApiService) HasUpdate() bool { + return !NoUpdate.Has(apiService.Name) +} + +// GetCloudProviderName() returns the name of the cloudprovider type for a service +func (apiService *ApiService) GetCloudProviderName() string { + result, ok := MainServices[apiService.Name] + if !ok { + panic(fmt.Errorf("%s not present in map: %v", apiService.Name, MainServices)) + } + + return result +} + +var AllApiServices []ApiService + +// createVarName() converts the service name into camelcase +func createVarName(str string) string { + copy := []rune(str) + if len(copy) == 0 { + return string(copy) + } + + copy[0] = unicode.ToLower(rune(copy[0])) + return string(copy) +} + +// populateApiServices() parses the Api Spec and populates AllApiServices with the required services +// Performs BFS to resolve dependencies +func populateApiServices() { + apiFile, err := os.Open(apiFilePath) + if err != nil { + panic(err) + } + defer apiFile.Close() + + byteValue, err := ioutil.ReadAll(apiFile) + if err != nil { + panic(err) + } + + var result map[string]interface{} + json.Unmarshal([]byte(byteValue), &result) + + // Queue of ApiService names for BFS + typesQueue := []string{} + // Set of already parsed ApiService names for BFS + completed := sets.String{} + // Go type of the property + var propType string + + keys := []string{} + for key := range MainServices { + keys = append(keys, key) + } + typesQueue = append(typesQueue, keys...) + + for len(typesQueue) > 0 { + typeName := typesQueue[0] + typesQueue = typesQueue[1:] + + if completed.Has(typeName) { + continue + } + completed.Insert(typeName) + + fields, ok := result["schemas"].(map[string]interface{})[typeName].(map[string]interface{})["properties"].(map[string]interface{}) + if !ok { + panic(fmt.Errorf("Unable to parse type: %s", typeName)) + } + + apiService := ApiService{Name: typeName, Fields: []ApiService{}, VarName: createVarName(typeName)} + + for prop, val := range fields { + subType := ApiService{Name: strings.Title(prop), JsonName: prop} + + var override bool + propType, typesQueue, override, err = getGoType(val, typesQueue) + if err != nil { + panic(err) + } + subType.GoType = propType + subType.JsonStringOverride = override + apiService.Fields = append(apiService.Fields, subType) + } + + // Sort fields since the keys aren't ordered deterministically + sort.Slice(apiService.Fields[:], func(i, j int) bool { + return apiService.Fields[i].Name < apiService.Fields[j].Name + }) + + AllApiServices = append(AllApiServices, apiService) + } + + // Sort the struct definitions since the keys aren't ordered deterministically + sort.Slice(AllApiServices[:], func(i, j int) bool { + return AllApiServices[i].Name < AllApiServices[j].Name + }) +} + +// getGoType() determines what the golang type is for a service by recursively descending the API spec json +// for a field. Since this may discover new types, it also updates the typesQueue. +func getGoType(val interface{}, typesQueue []string) (string, []string, bool, error) { + field, ok := val.(map[string]interface{}) + if !ok { + panic(nil) + } + + var err error + var tmpType string + var override bool + + propType := "" + ref, ok := field["$ref"] + // Field is not a built-in type, we need to wrap it + if ok { + refName := ref.(string) + typesQueue = append(typesQueue, refName) + propType = "*" + refName + } else if field["type"] == "array" { + tmpType, typesQueue, override, err = getGoType(field["items"], typesQueue) + propType = "[]" + tmpType + } else if field["type"] == "object" { + addlProps, ok := field["additionalProperties"] + if ok { + tmpType, typesQueue, override, err = getGoType(addlProps, typesQueue) + propType = "map[string]" + tmpType + } else { + propType = "map[string]string" + } + } else if format, ok := field["format"]; ok { + if format.(string) == "byte" { + propType = "string" + } else if format.(string) == "float" { + propType = "float64" + } else if format.(string) == "int32" { + propType = "int64" + } else { + propType = format.(string) + } + } else if field["type"] != "" { + if field["type"].(string) == "boolean" { + propType = "bool" + } else { + propType = field["type"].(string) + } + } else { + err = fmt.Errorf("unable to get property type for prop: %v", val) + } + + if field["type"] == "string" && propType != "string" { + override = true + } + + return propType, typesQueue, override, err +} + +func init() { + AllApiServices = []ApiService{} + populateApiServices() +}