Merge pull request #57879 from bowei/gce-gen

Automatic merge from submit-queue (batch tested with PRs 58025, 57112, 57879, 57571, 58062). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Code generation for GCE compute interface

Use code generation to "write" most of the GCE cloud provider library. This enables the following:

- Consistent interfaces, including handling of the different API versions (GA, alpha, beta)
- Efficient implementation of cross cutting features such as metrics, logging, tracing etc. Adding such features has in the past been a tedious and error prone endeavor. 
- High fidelity mocks for all of the compute API. What this means is that most of our controller logic can be tested as unit tests in a consistent way without creating individual mocks by hand.

```release-note
NONE
```
This commit is contained in:
Kubernetes Submit Queue 2018-01-10 11:46:46 -08:00 committed by GitHub
commit b873fc4453
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 16228 additions and 1 deletions

View File

@ -87,6 +87,7 @@ pkg/cloudprovider
pkg/cloudprovider/providers/aws
pkg/cloudprovider/providers/fake
pkg/cloudprovider/providers/gce
pkg/cloudprovider/providers/gce/cloud
pkg/cloudprovider/providers/openstack
pkg/cloudprovider/providers/ovirt
pkg/cloudprovider/providers/photon

View File

@ -0,0 +1,38 @@
#!/bin/bash
# Copyright 2018 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.
set -o errexit
set -o nounset
set -o pipefail
KUBE_ROOT=$(dirname "${BASH_SOURCE}")/..
source "${KUBE_ROOT}/hack/lib/init.sh"
GENERATOR="${KUBE_ROOT}/pkg/cloudprovider/providers/gce/cloud/gen/main.go"
GEN_GO="${KUBE_ROOT}/pkg/cloudprovider/providers/gce/cloud/gen.go"
GEN_TEST_GO="${KUBE_ROOT}/pkg/cloudprovider/providers/gce/cloud/gen_test.go"
kube::golang::setup_env
TMPFILE=$(mktemp verify-cloudprovider-gce-XXXX)
trap "{ rm -f ${TMPFILE}; }" EXIT
go run "${GENERATOR}" > ${TMPFILE}
mv "${TMPFILE}" "${GEN_GO}"
go run "${GENERATOR}" -mode test > ${TMPFILE}
mv "${TMPFILE}" "${GEN_TEST_GO}"
exit 0

View File

@ -0,0 +1,49 @@
#!/bin/bash
# Copyright 2018 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.
set -o errexit
set -o nounset
set -o pipefail
KUBE_ROOT=$(dirname "${BASH_SOURCE}")/..
source "${KUBE_ROOT}/hack/lib/init.sh"
GENERATOR="${KUBE_ROOT}/pkg/cloudprovider/providers/gce/cloud/gen/main.go"
GEN_GO="${KUBE_ROOT}/pkg/cloudprovider/providers/gce/cloud/gen.go"
GEN_TEST_GO="${KUBE_ROOT}/pkg/cloudprovider/providers/gce/cloud/gen_test.go"
kube::golang::setup_env
TMPFILE=$(mktemp verify-cloudprovider-gce-XXXX)
trap "{ rm -f ${TMPFILE}; }" EXIT
go run "${GENERATOR}" > ${TMPFILE}
if ! diff "${TMPFILE}" "${GEN_GO}"; then
echo "Generated file ${GEN_GO} needs to be updated (run hack/update-cloudprovider-gce.sh)"
echo
diff -u "${TMPFILE}" "${GEN_GO}" || true
exit 1
fi
go run "${GENERATOR}" -mode test > ${TMPFILE}
if ! diff "${TMPFILE}" "${GEN_TEST_GO}"; then
echo "Generated file ${GEN_TEST_GO} needs to be updated (run hack/update-cloudprovider-gce.sh)"
echo
diff -u "${TMPFILE}" "${GEN_TEST_GO}" || true
exit 1
fi
exit 0

View File

@ -125,6 +125,9 @@ filegroup(
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
srcs = [
":package-srcs",
"//pkg/cloudprovider/providers/gce/cloud:all-srcs",
],
tags = ["automanaged"],
)

View File

@ -0,0 +1,63 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"gce_projects.go",
"gen.go",
"op.go",
"project.go",
"ratelimit.go",
"service.go",
"utils.go",
],
importpath = "k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud",
visibility = ["//visibility:public"],
deps = [
"//pkg/cloudprovider/providers/gce/cloud/filter:go_default_library",
"//pkg/cloudprovider/providers/gce/cloud/meta:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/google.golang.org/api/compute/v0.alpha:go_default_library",
"//vendor/google.golang.org/api/compute/v0.beta:go_default_library",
"//vendor/google.golang.org/api/compute/v1:go_default_library",
"//vendor/google.golang.org/api/googleapi:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"gen_test.go",
"mock_test.go",
"utils_test.go",
],
embed = [":go_default_library"],
importpath = "k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud",
deps = [
"//pkg/cloudprovider/providers/gce/cloud/filter:go_default_library",
"//pkg/cloudprovider/providers/gce/cloud/meta:go_default_library",
"//vendor/google.golang.org/api/compute/v0.alpha:go_default_library",
"//vendor/google.golang.org/api/compute/v0.beta:go_default_library",
"//vendor/google.golang.org/api/compute/v1:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/cloudprovider/providers/gce/cloud/filter:all-srcs",
"//pkg/cloudprovider/providers/gce/cloud/gen:all-srcs",
"//pkg/cloudprovider/providers/gce/cloud/meta:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,112 @@
/*
Copyright 2017 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.
*/
// Package cloud implements a more golang friendly interface to the GCE compute
// API. The code in this package is generated automatically via the generator
// implemented in "gen/main.go". The code generator creates the basic CRUD
// actions for the given resource: "Insert", "Get", "List" and "Delete".
// Additional methods by customizing the ServiceInfo object (see below).
// Generated code includes a full mock of the GCE compute API.
//
// Usage
//
// The root of the GCE compute API is the interface "Cloud". Code written using
// Cloud can be used against the actual implementation "GCE" or "MockGCE".
//
// func foo(cloud Cloud) {
// igs, err := cloud.InstanceGroups().List(ctx, "us-central1-b", filter.None)
// ...
// }
// // Run foo against the actual cloud.
// foo(NewGCE(&Service{...}))
// // Run foo with a mock.
// foo(NewMockGCE())
//
// Rate limiting and routing
//
// The generated code allows for custom policies for operation rate limiting
// and GCE project routing. See RateLimiter and ProjectRouter for more details.
//
// Mocks
//
// Mocks are automatically generated for each type implementing basic logic for
// resource manipulation. This eliminates the boilerplate required to mock GCE
// functionality. Each method will also have a corresponding "xxxHook"
// function generated in the mock structure where unit test code can hook the
// execution of the method.
//
// Mocks for different versions of the same service will share the same set of
// objects, i.e. an alpha object will be visible with beta and GA methods.
// Note that translation is done with JSON serialization between the API versions.
//
// Changing service code generation
//
// The list of services to generate is contained in "meta/meta.go". To add a
// service, add an entry to the list "meta.AllServices". An example entry:
//
// &ServiceInfo{
// Object: "InstanceGroup", // Name of the object type.
// Service: "InstanceGroups", // Name of the service.
// Resource: "instanceGroups", // Lowercase resource name (as appears in the URL).
// version: meta.VersionAlpha, // API version (one entry per version is needed).
// keyType: Zonal, // What kind of resource this is.
// serviceType: reflect.TypeOf(&alpha.InstanceGroupsService{}), // Associated golang type.
// additionalMethods: []string{ // Additional methods to generate code for.
// "SetNamedPorts",
// },
// options: <options> // Or'd ("|") together.
// }
//
// Read-only objects
//
// Services such as Regions and Zones do not allow for mutations. Specify
// "ReadOnly" in ServiceInfo.options to omit the mutation methods.
//
// Adding custom methods
//
// Some methods that may not be properly handled by the generated code. To enable
// addition of custom code to the generated mocks, set the "CustomOps" option
// in "meta.ServiceInfo" entry. This will make the generated service interface
// embed a "<ServiceName>Ops" interface. This interface MUST be written by hand
// and contain the custom method logic. Corresponding methods must be added to
// the corresponding Mockxxx and GCExxx struct types.
//
// // In "meta/meta.go":
// &ServiceInfo{
// Object: "InstanceGroup",
// ...
// options: CustomOps,
// }
//
// // In the generated code "gen.go":
// type InstanceGroups interface {
// InstanceGroupsOps // Added by CustomOps option.
// ...
// }
//
// // In hand written file:
// type InstanceGroupsOps interface {
// MyMethod()
// }
//
// func (mock *MockInstanceGroups) MyMethod() {
// // Custom mock implementation.
// }
//
// func (gce *GCEInstanceGroups) MyMethod() {
// // Custom implementation.
// }
package cloud

View File

@ -0,0 +1,30 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["filter.go"],
importpath = "k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/filter",
visibility = ["//visibility:public"],
deps = ["//vendor/github.com/golang/glog:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["filter_test.go"],
embed = [":go_default_library"],
importpath = "k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/filter",
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,303 @@
/*
Copyright 2017 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.
*/
// Package filter encapsulates the filter argument to compute API calls.
//
// // List all global addresses (no filter).
// c.GlobalAddresses().List(ctx, filter.None)
//
// // List global addresses filtering for name matching "abc.*".
// c.GlobalAddresses().List(ctx, filter.Regexp("name", "abc.*"))
//
// // List on multiple conditions.
// f := filter.Regexp("name", "homer.*").AndNotRegexp("name", "homers")
// c.GlobalAddresses().List(ctx, f)
package filter
import (
"errors"
"fmt"
"reflect"
"regexp"
"strings"
"github.com/golang/glog"
)
var (
// None indicates that the List result set should not be filter (i.e.
// return all values).
None *F
)
// Regexp returns a filter for fieldName matches regexp v.
func Regexp(fieldName, v string) *F {
return (&F{}).AndRegexp(fieldName, v)
}
// NotRegexp returns a filter for fieldName not matches regexp v.
func NotRegexp(fieldName, v string) *F {
return (&F{}).AndNotRegexp(fieldName, v)
}
// EqualInt returns a filter for fieldName ~ v.
func EqualInt(fieldName string, v int) *F {
return (&F{}).AndEqualInt(fieldName, v)
}
// NotEqualInt returns a filter for fieldName != v.
func NotEqualInt(fieldName string, v int) *F {
return (&F{}).AndNotEqualInt(fieldName, v)
}
// EqualBool returns a filter for fieldName == v.
func EqualBool(fieldName string, v bool) *F {
return (&F{}).AndEqualBool(fieldName, v)
}
// NotEqualBool returns a filter for fieldName != v.
func NotEqualBool(fieldName string, v bool) *F {
return (&F{}).AndNotEqualBool(fieldName, v)
}
// F is a filter to be used with List() operations.
//
// From the compute API description:
//
// Sets a filter {expression} for filtering listed resources. Your {expression}
// must be in the format: field_name comparison_string literal_string.
//
// The field_name is the name of the field you want to compare. Only atomic field
// types are supported (string, number, boolean). The comparison_string must be
// either eq (equals) or ne (not equals). The literal_string is the string value
// to filter to. The literal value must be valid for the type of field you are
// filtering by (string, number, boolean). For string fields, the literal value is
// interpreted as a regular expression using RE2 syntax. The literal value must
// match the entire field.
//
// For example, to filter for instances that do not have a name of
// example-instance, you would use name ne example-instance.
//
// You can filter on nested fields. For example, you could filter on instances
// that have set the scheduling.automaticRestart field to true. Use filtering on
// nested fields to take advantage of labels to organize and search for results
// based on label values.
//
// To filter on multiple expressions, provide each separate expression within
// parentheses. For example, (scheduling.automaticRestart eq true)
// (zone eq us-central1-f). Multiple expressions are treated as AND expressions,
// meaning that resources must match all expressions to pass the filters.
type F struct {
predicates []filterPredicate
}
// And joins two filters together.
func (fl *F) And(rest *F) *F {
fl.predicates = append(fl.predicates, rest.predicates...)
return fl
}
// AndRegexp adds a field match string predicate.
func (fl *F) AndRegexp(fieldName, v string) *F {
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: equals, s: &v})
return fl
}
// AndNotRegexp adds a field not match string predicate.
func (fl *F) AndNotRegexp(fieldName, v string) *F {
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: notEquals, s: &v})
return fl
}
// AndEqualInt adds a field == int predicate.
func (fl *F) AndEqualInt(fieldName string, v int) *F {
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: equals, i: &v})
return fl
}
// AndNotEqualInt adds a field != int predicate.
func (fl *F) AndNotEqualInt(fieldName string, v int) *F {
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: notEquals, i: &v})
return fl
}
// AndEqualBool adds a field == bool predicate.
func (fl *F) AndEqualBool(fieldName string, v bool) *F {
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: equals, b: &v})
return fl
}
// AndNotEqualBool adds a field != bool predicate.
func (fl *F) AndNotEqualBool(fieldName string, v bool) *F {
fl.predicates = append(fl.predicates, filterPredicate{fieldName: fieldName, op: notEquals, b: &v})
return fl
}
func (fl *F) String() string {
if len(fl.predicates) == 1 {
return fl.predicates[0].String()
}
var pl []string
for _, p := range fl.predicates {
pl = append(pl, "("+p.String()+")")
}
return strings.Join(pl, " ")
}
// Match returns true if the F as specifies matches the given object. This
// is used by the Mock implementations to perform filtering and SHOULD NOT be
// used in production code as it is not well-tested to be equivalent to the
// actual compute API.
func (fl *F) Match(obj interface{}) bool {
if fl == nil {
return true
}
for _, p := range fl.predicates {
if !p.match(obj) {
return false
}
}
return true
}
type filterOp int
const (
equals filterOp = iota
notEquals filterOp = iota
)
// filterPredicate is an individual predicate for a fieldName and value.
type filterPredicate struct {
fieldName string
op filterOp
s *string
i *int
b *bool
}
func (fp *filterPredicate) String() string {
var op string
switch fp.op {
case equals:
op = "eq"
case notEquals:
op = "ne"
default:
op = "invalidOp"
}
var value string
switch {
case fp.s != nil:
// There does not seem to be any sort of escaping as specified in the
// document. This means it's possible to create malformed expressions.
value = *fp.s
case fp.i != nil:
value = fmt.Sprintf("%d", *fp.i)
case fp.b != nil:
value = fmt.Sprintf("%t", *fp.b)
default:
value = "invalidValue"
}
return fmt.Sprintf("%s %s %s", fp.fieldName, op, value)
}
func (fp *filterPredicate) match(o interface{}) bool {
v, err := extractValue(fp.fieldName, o)
glog.V(6).Infof("extractValue(%q, %#v) = %v, %v", fp.fieldName, o, v, err)
if err != nil {
return false
}
var match bool
switch x := v.(type) {
case string:
if fp.s == nil {
return false
}
re, err := regexp.Compile(*fp.s)
if err != nil {
glog.Errorf("Match regexp %q is invalid: %v", *fp.s, err)
return false
}
match = re.Match([]byte(x))
case int:
if fp.i == nil {
return false
}
match = x == *fp.i
case bool:
if fp.b == nil {
return false
}
match = x == *fp.b
}
switch fp.op {
case equals:
return match
case notEquals:
return !match
}
return false
}
// snakeToCamelCase converts from "names_like_this" to "NamesLikeThis" to
// interoperate between proto and Golang naming conventions.
func snakeToCamelCase(s string) string {
parts := strings.Split(s, "_")
var ret string
for _, x := range parts {
ret += strings.Title(x)
}
return ret
}
// extractValue returns the value of the field named by path in object o if it exists.
func extractValue(path string, o interface{}) (interface{}, error) {
parts := strings.Split(path, ".")
for _, f := range parts {
v := reflect.ValueOf(o)
// Dereference Ptr to handle *struct.
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil, errors.New("field is nil")
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, fmt.Errorf("cannot get field from non-struct (%T)", o)
}
v = v.FieldByName(snakeToCamelCase(f))
if !v.IsValid() {
return nil, fmt.Errorf("cannot get field %q as it is not a valid field in %T", f, o)
}
if !v.CanInterface() {
return nil, fmt.Errorf("cannot get field %q in obj of type %T", f, o)
}
o = v.Interface()
}
switch o.(type) {
case string, int, bool:
return o, nil
}
return nil, fmt.Errorf("unhandled object of type %T", o)
}

View File

@ -0,0 +1,176 @@
/*
Copyright 2017 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.
*/
package filter
import (
"reflect"
"testing"
)
func TestFilterToString(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
f *F
want string
}{
{Regexp("field1", "abc"), `field1 eq abc`},
{NotRegexp("field1", "abc"), `field1 ne abc`},
{EqualInt("field1", 13), "field1 eq 13"},
{NotEqualInt("field1", 13), "field1 ne 13"},
{EqualBool("field1", true), "field1 eq true"},
{NotEqualBool("field1", true), "field1 ne true"},
{Regexp("field1", "abc").AndRegexp("field2", "def"), `(field1 eq abc) (field2 eq def)`},
{Regexp("field1", "abc").AndNotEqualInt("field2", 17), `(field1 eq abc) (field2 ne 17)`},
{Regexp("field1", "abc").And(EqualInt("field2", 17)), `(field1 eq abc) (field2 eq 17)`},
} {
if tc.f.String() != tc.want {
t.Errorf("filter %#v String() = %q, want %q", tc.f, tc.f.String(), tc.want)
}
}
}
func TestFilterMatch(t *testing.T) {
t.Parallel()
type inner struct {
X string
}
type S struct {
S string
I int
B bool
Unhandled struct{}
NestedField *inner
}
for _, tc := range []struct {
f *F
o interface{}
want bool
}{
{f: None, o: &S{}, want: true},
{f: Regexp("s", "abc"), o: &S{}},
{f: EqualInt("i", 10), o: &S{}},
{f: EqualBool("b", true), o: &S{}},
{f: NotRegexp("s", "abc"), o: &S{}, want: true},
{f: NotEqualInt("i", 10), o: &S{}, want: true},
{f: NotEqualBool("b", true), o: &S{}, want: true},
{f: Regexp("s", "abc").AndEqualBool("b", true), o: &S{}},
{f: Regexp("s", "abc"), o: &S{S: "abc"}, want: true},
{f: Regexp("s", "a.*"), o: &S{S: "abc"}, want: true},
{f: Regexp("s", "a((("), o: &S{S: "abc"}},
{f: NotRegexp("s", "abc"), o: &S{S: "abc"}},
{f: EqualInt("i", 10), o: &S{I: 11}},
{f: EqualInt("i", 10), o: &S{I: 10}, want: true},
{f: Regexp("s", "abc").AndEqualBool("b", true), o: &S{S: "abc"}},
{f: Regexp("s", "abcd").AndEqualBool("b", true), o: &S{S: "abc"}},
{f: Regexp("s", "abc").AndEqualBool("b", true), o: &S{S: "abc", B: true}, want: true},
{f: Regexp("s", "abc").And(EqualBool("b", true)), o: &S{S: "abc", B: true}, want: true},
{f: Regexp("unhandled", "xyz"), o: &S{}},
{f: Regexp("nested_field.x", "xyz"), o: &S{}},
{f: Regexp("nested_field.x", "xyz"), o: &S{NestedField: &inner{"xyz"}}, want: true},
{f: NotRegexp("nested_field.x", "xyz"), o: &S{NestedField: &inner{"xyz"}}},
{f: Regexp("nested_field.y", "xyz"), o: &S{NestedField: &inner{"xyz"}}},
{f: Regexp("nested_field", "xyz"), o: &S{NestedField: &inner{"xyz"}}},
} {
got := tc.f.Match(tc.o)
if got != tc.want {
t.Errorf("%v: Match(%+v) = %v, want %v", tc.f, tc.o, got, tc.want)
}
}
}
func TestFilterSnakeToCamelCase(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
s string
want string
}{
{"", ""},
{"abc", "Abc"},
{"_foo", "Foo"},
{"a_b_c", "ABC"},
{"a_BC_def", "ABCDef"},
{"a_Bc_def", "ABcDef"},
} {
got := snakeToCamelCase(tc.s)
if got != tc.want {
t.Errorf("snakeToCamelCase(%q) = %q, want %q", tc.s, got, tc.want)
}
}
}
func TestFilterExtractValue(t *testing.T) {
t.Parallel()
type nest2 struct {
Y string
}
type nest struct {
X string
Nest2 nest2
}
st := &struct {
S string
I int
F bool
Nest nest
NestPtr *nest
Unhandled float64
}{
"abc",
13,
true,
nest{"xyz", nest2{"zzz"}},
&nest{"yyy", nest2{}},
0.0,
}
for _, tc := range []struct {
path string
o interface{}
want interface{}
wantErr bool
}{
{path: "s", o: st, want: "abc"},
{path: "i", o: st, want: 13},
{path: "f", o: st, want: true},
{path: "nest.x", o: st, want: "xyz"},
{path: "nest_ptr.x", o: st, want: "yyy"},
// Error cases.
{path: "", o: st, wantErr: true},
{path: "no_such_field", o: st, wantErr: true},
{path: "s.invalid_type", o: st, wantErr: true},
{path: "unhandled", o: st, wantErr: true},
{path: "nest.x", o: &struct{ Nest *nest }{}, wantErr: true},
} {
o, err := extractValue(tc.path, tc.o)
gotErr := err != nil
if gotErr != tc.wantErr {
t.Errorf("extractValue(%v, %+v) = %v, %v; gotErr = %v, tc.wantErr = %v", tc.path, tc.o, o, err, gotErr, tc.wantErr)
}
if err != nil {
continue
}
if !reflect.DeepEqual(o, tc.want) {
t.Errorf("extractValue(%v, %+v) = %v, nil; want %v, nil", tc.path, tc.o, o, tc.want)
}
}
}

View File

@ -0,0 +1,99 @@
/*
Copyright 2017 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.
*/
package cloud
import (
"context"
"fmt"
"net/http"
compute "google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/meta"
)
// ProjectsOps is the manually implemented methods for the Projects service.
type ProjectsOps interface {
Get(ctx context.Context, projectID string) (*compute.Project, error)
SetCommonInstanceMetadata(ctx context.Context, projectID string, m *compute.Metadata) error
}
// MockProjectOpsState is stored in the mock.X field.
type MockProjectOpsState struct {
metadata map[string]*compute.Metadata
}
// Get a project by projectID.
func (m *MockProjects) Get(ctx context.Context, projectID string) (*compute.Project, error) {
m.Lock.Lock()
defer m.Lock.Unlock()
if p, ok := m.Objects[*meta.GlobalKey(projectID)]; ok {
return p.ToGA(), nil
}
return nil, &googleapi.Error{
Code: http.StatusNotFound,
Message: fmt.Sprintf("MockProjects %v not found", projectID),
}
}
// Get a project by projectID.
func (g *GCEProjects) Get(ctx context.Context, projectID string) (*compute.Project, error) {
rk := &RateLimitKey{
ProjectID: projectID,
Operation: "Get",
Version: meta.Version("ga"),
Service: "Projects",
}
if err := g.s.RateLimiter.Accept(ctx, rk); err != nil {
return nil, err
}
call := g.s.GA.Projects.Get(projectID)
call.Context(ctx)
return call.Do()
}
// SetCommonInstanceMetadata for a given project.
func (m *MockProjects) SetCommonInstanceMetadata(ctx context.Context, projectID string, meta *compute.Metadata) error {
if m.X == nil {
m.X = &MockProjectOpsState{metadata: map[string]*compute.Metadata{}}
}
state := m.X.(*MockProjectOpsState)
state.metadata[projectID] = meta
return nil
}
// SetCommonInstanceMetadata for a given project.
func (g *GCEProjects) SetCommonInstanceMetadata(ctx context.Context, projectID string, m *compute.Metadata) error {
rk := &RateLimitKey{
ProjectID: projectID,
Operation: "SetCommonInstanceMetadata",
Version: meta.Version("ga"),
Service: "Projects",
}
if err := g.s.RateLimiter.Accept(ctx, rk); err != nil {
return err
}
call := g.s.GA.Projects.SetCommonInstanceMetadata(projectID, m)
call.Context(ctx)
op, err := call.Do()
if err != nil {
return err
}
return g.s.WaitForCompletion(ctx, op)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/gen",
visibility = ["//visibility:private"],
deps = ["//pkg/cloudprovider/providers/gce/cloud/meta:go_default_library"],
)
go_binary(
name = "gen",
embed = [":go_default_library"],
importpath = "k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/gen",
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"key.go",
"meta.go",
"method.go",
"service.go",
],
importpath = "k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/meta",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/google.golang.org/api/compute/v0.alpha:go_default_library",
"//vendor/google.golang.org/api/compute/v0.beta:go_default_library",
"//vendor/google.golang.org/api/compute/v1:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["key_test.go"],
embed = [":go_default_library"],
importpath = "k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/meta",
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,19 @@
/*
Copyright 2017 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.
*/
// Package meta contains the meta description of the GCE cloud types to
// generate code for.
package meta

View File

@ -0,0 +1,96 @@
/*
Copyright 2017 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.
*/
package meta
import (
"fmt"
)
// Key for a GCP resource.
type Key struct {
Name string
Zone string
Region string
}
// KeyType is the type of the key.
type KeyType string
const (
// Zonal key type.
Zonal = "zonal"
// Regional key type.
Regional = "regional"
// Global key type.
Global = "global"
)
// ZonalKey returns the key for a zonal resource.
func ZonalKey(name, zone string) *Key {
return &Key{name, zone, ""}
}
// RegionalKey returns the key for a regional resource.
func RegionalKey(name, region string) *Key {
return &Key{name, "", region}
}
// GlobalKey returns the key for a global resource.
func GlobalKey(name string) *Key {
return &Key{name, "", ""}
}
// Type returns the type of the key.
func (k *Key) Type() KeyType {
switch {
case k.Zone != "":
return Zonal
case k.Region != "":
return Regional
default:
return Global
}
}
// String returns a string representation of the key.
func (k Key) String() string {
switch k.Type() {
case Zonal:
return fmt.Sprintf("Key{%q, zone: %q}", k.Name, k.Zone)
case Regional:
return fmt.Sprintf("Key{%q, region: %q}", k.Name, k.Region)
default:
return fmt.Sprintf("Key{%q}", k.Name)
}
}
// Valid is true if the key is valid.
func (k *Key) Valid(typeName string) bool {
if k.Zone != "" && k.Region != "" {
return false
}
return true
}
// KeysToMap creates a map[Key]bool from a list of keys.
func KeysToMap(keys ...Key) map[Key]bool {
ret := map[Key]bool{}
for _, k := range keys {
ret[k] = true
}
return ret
}

View File

@ -0,0 +1,75 @@
/*
Copyright 2017 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.
*/
package meta
import (
"testing"
)
func TestKeyType(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
key *Key
want KeyType
}{
{GlobalKey("abc"), Global},
{ZonalKey("abc", "us-central1-b"), Zonal},
{RegionalKey("abc", "us-central1"), Regional},
} {
if tc.key.Type() != tc.want {
t.Errorf("key.Type() == %v, want %v", tc.key.Type(), tc.want)
}
}
}
func TestKeyString(t *testing.T) {
t.Parallel()
for _, k := range []*Key{
GlobalKey("abc"),
RegionalKey("abc", "us-central1"),
ZonalKey("abc", "us-central1-b"),
} {
if k.String() == "" {
t.Errorf(`k.String() = "", want non-empty`)
}
}
}
func TestKeyValid(t *testing.T) {
t.Parallel()
region := "us-central1"
zone := "us-central1-b"
for _, tc := range []struct {
key *Key
typeName string
want bool
}{
// Note: these test cases need to be synchronized with the
// actual settings for each type.
{GlobalKey("abc"), "UrlMap", true},
{&Key{"abc", zone, region}, "UrlMap", false},
} {
valid := tc.key.Valid(tc.typeName)
if valid != tc.want {
t.Errorf("key %+v, type %v; key.Valid() = %v, want %v", tc.key, tc.typeName, valid, tc.want)
}
}
}

View File

@ -0,0 +1,372 @@
/*
Copyright 2017 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.
*/
package meta
import (
"reflect"
alpha "google.golang.org/api/compute/v0.alpha"
beta "google.golang.org/api/compute/v0.beta"
ga "google.golang.org/api/compute/v1"
)
// Version of the API (ga, alpha, beta).
type Version string
const (
// NoGet prevents the Get() method from being generated.
NoGet = 1 << iota
// NoList prevents the List() method from being generated.
NoList = 1 << iota
// NoDelete prevents the Delete() method from being generated.
NoDelete = 1 << iota
// NoInsert prevents the Insert() method from being generated.
NoInsert = 1 << iota
// CustomOps specifies that an empty interface xxxOps will be generated to
// enable custom method calls to be attached to the generated service
// interface.
CustomOps = 1 << iota
// AggregatedList will generated a method for AggregatedList().
AggregatedList = 1 << iota
// ReadOnly specifies that the given resource is read-only and should not
// have insert() or delete() methods generated for the wrapper.
ReadOnly = NoDelete | NoInsert
// VersionGA is the API version in compute.v1.
VersionGA Version = "ga"
// VersionAlpha is the API version in computer.v0.alpha.
VersionAlpha Version = "alpha"
// VersionBeta is the API version in computer.v0.beta.
VersionBeta Version = "beta"
)
// AllVersions is a list of all versions of the GCE API.
var AllVersions = []Version{
VersionGA,
VersionAlpha,
VersionBeta,
}
// AllServices are a list of all the services to generate code for. Keep
// this list in lexiographical order by object type.
var AllServices = []*ServiceInfo{
{
Object: "Address",
Service: "Addresses",
Resource: "addresses",
keyType: Regional,
serviceType: reflect.TypeOf(&ga.AddressesService{}),
},
{
Object: "Address",
Service: "Addresses",
Resource: "addresses",
version: VersionAlpha,
keyType: Regional,
serviceType: reflect.TypeOf(&alpha.AddressesService{}),
},
{
Object: "Address",
Service: "Addresses",
Resource: "addresses",
version: VersionBeta,
keyType: Regional,
serviceType: reflect.TypeOf(&beta.AddressesService{}),
},
{
Object: "Address",
Service: "GlobalAddresses",
Resource: "addresses",
keyType: Global,
serviceType: reflect.TypeOf(&ga.GlobalAddressesService{}),
},
{
Object: "BackendService",
Service: "BackendServices",
Resource: "backendServices",
keyType: Global,
serviceType: reflect.TypeOf(&ga.BackendServicesService{}),
additionalMethods: []string{
"GetHealth",
"Update",
},
},
{
Object: "BackendService",
Service: "BackendServices",
Resource: "backendServices",
version: VersionAlpha,
keyType: Global,
serviceType: reflect.TypeOf(&alpha.BackendServicesService{}),
additionalMethods: []string{"Update"},
},
{
Object: "BackendService",
Service: "RegionBackendServices",
Resource: "backendServices",
version: VersionAlpha,
keyType: Regional,
serviceType: reflect.TypeOf(&alpha.RegionBackendServicesService{}),
additionalMethods: []string{
"GetHealth",
"Update",
},
},
{
Object: "Disk",
Service: "Disks",
Resource: "disks",
keyType: Zonal,
serviceType: reflect.TypeOf(&ga.DisksService{}),
},
{
Object: "Disk",
Service: "Disks",
Resource: "disks",
version: VersionAlpha,
keyType: Zonal,
serviceType: reflect.TypeOf(&alpha.DisksService{}),
},
{
Object: "Disk",
Service: "RegionDisks",
Resource: "disks",
version: VersionAlpha,
keyType: Regional,
serviceType: reflect.TypeOf(&alpha.DisksService{}),
},
{
Object: "Firewall",
Service: "Firewalls",
Resource: "firewalls",
keyType: Global,
serviceType: reflect.TypeOf(&ga.FirewallsService{}),
additionalMethods: []string{
"Update",
},
},
{
Object: "ForwardingRule",
Service: "ForwardingRules",
Resource: "forwardingRules",
keyType: Regional,
serviceType: reflect.TypeOf(&ga.ForwardingRulesService{}),
},
{
Object: "ForwardingRule",
Service: "ForwardingRules",
Resource: "forwardingRules",
version: VersionAlpha,
keyType: Regional,
serviceType: reflect.TypeOf(&alpha.ForwardingRulesService{}),
},
{
Object: "ForwardingRule",
Service: "GlobalForwardingRules",
Resource: "forwardingRules",
keyType: Global,
serviceType: reflect.TypeOf(&ga.GlobalForwardingRulesService{}),
additionalMethods: []string{
"SetTarget",
},
},
{
Object: "HealthCheck",
Service: "HealthChecks",
Resource: "healthChecks",
keyType: Global,
serviceType: reflect.TypeOf(&ga.HealthChecksService{}),
additionalMethods: []string{
"Update",
},
},
{
Object: "HealthCheck",
Service: "HealthChecks",
Resource: "healthChecks",
version: VersionAlpha,
keyType: Global,
serviceType: reflect.TypeOf(&alpha.HealthChecksService{}),
additionalMethods: []string{
"Update",
},
},
{
Object: "HttpHealthCheck",
Service: "HttpHealthChecks",
Resource: "httpHealthChecks",
keyType: Global,
serviceType: reflect.TypeOf(&ga.HttpHealthChecksService{}),
additionalMethods: []string{
"Update",
},
},
{
Object: "HttpsHealthCheck",
Service: "HttpsHealthChecks",
Resource: "httpsHealthChecks",
keyType: Global,
serviceType: reflect.TypeOf(&ga.HttpsHealthChecksService{}),
additionalMethods: []string{
"Update",
},
},
{
Object: "InstanceGroup",
Service: "InstanceGroups",
Resource: "instanceGroups",
keyType: Zonal,
serviceType: reflect.TypeOf(&ga.InstanceGroupsService{}),
additionalMethods: []string{
"AddInstances",
"ListInstances",
"RemoveInstances",
"SetNamedPorts",
},
},
{
Object: "Instance",
Service: "Instances",
Resource: "instances",
keyType: Zonal,
serviceType: reflect.TypeOf(&ga.InstancesService{}),
additionalMethods: []string{
"AttachDisk",
"DetachDisk",
},
},
{
Object: "Instance",
Service: "Instances",
Resource: "instances",
version: VersionBeta,
keyType: Zonal,
serviceType: reflect.TypeOf(&beta.InstancesService{}),
additionalMethods: []string{
"AttachDisk",
"DetachDisk",
},
},
{
Object: "Instance",
Service: "Instances",
Resource: "instances",
version: VersionAlpha,
keyType: Zonal,
serviceType: reflect.TypeOf(&alpha.InstancesService{}),
additionalMethods: []string{
"AttachDisk",
"DetachDisk",
"UpdateNetworkInterface",
},
},
{
Object: "NetworkEndpointGroup",
Service: "NetworkEndpointGroups",
Resource: "networkEndpointGroups",
version: VersionAlpha,
keyType: Zonal,
serviceType: reflect.TypeOf(&alpha.NetworkEndpointGroupsService{}),
additionalMethods: []string{
"AttachNetworkEndpoints",
"DetachNetworkEndpoints",
},
options: AggregatedList,
},
{
Object: "Project",
Service: "Projects",
Resource: "projects",
keyType: Global,
// Generate only the stub with no methods.
options: NoGet | NoList | NoInsert | NoDelete | CustomOps,
serviceType: reflect.TypeOf(&ga.ProjectsService{}),
},
{
Object: "Region",
Service: "Regions",
Resource: "regions",
keyType: Global,
options: ReadOnly,
serviceType: reflect.TypeOf(&ga.RegionsService{}),
},
{
Object: "Route",
Service: "Routes",
Resource: "routes",
keyType: Global,
serviceType: reflect.TypeOf(&ga.RoutesService{}),
},
{
Object: "SslCertificate",
Service: "SslCertificates",
Resource: "sslCertificates",
keyType: Global,
serviceType: reflect.TypeOf(&ga.SslCertificatesService{}),
},
{
Object: "TargetHttpProxy",
Service: "TargetHttpProxies",
Resource: "targetHttpProxies",
keyType: Global,
serviceType: reflect.TypeOf(&ga.TargetHttpProxiesService{}),
additionalMethods: []string{
"SetUrlMap",
},
},
{
Object: "TargetHttpsProxy",
Service: "TargetHttpsProxies",
Resource: "targetHttpsProxies",
keyType: Global,
serviceType: reflect.TypeOf(&ga.TargetHttpsProxiesService{}),
additionalMethods: []string{
"SetSslCertificates",
"SetUrlMap",
},
},
{
Object: "TargetPool",
Service: "TargetPools",
Resource: "targetPools",
keyType: Regional,
serviceType: reflect.TypeOf(&ga.TargetPoolsService{}),
additionalMethods: []string{
"AddInstance",
"RemoveInstance",
},
},
{
Object: "UrlMap",
Service: "UrlMaps",
Resource: "urlMaps",
keyType: Global,
serviceType: reflect.TypeOf(&ga.UrlMapsService{}),
additionalMethods: []string{
"Update",
},
},
{
Object: "Zone",
Service: "Zones",
Resource: "zones",
keyType: Global,
options: ReadOnly,
serviceType: reflect.TypeOf(&ga.ZonesService{}),
},
}

View File

@ -0,0 +1,250 @@
/*
Copyright 2017 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.
*/
package meta
import (
"fmt"
"reflect"
"strings"
"github.com/golang/glog"
)
func newArg(t reflect.Type) *arg {
ret := &arg{}
// Dereference the pointer types to get at the underlying concrete type.
Loop:
for {
switch t.Kind() {
case reflect.Ptr:
ret.numPtr++
t = t.Elem()
default:
ret.pkg = t.PkgPath()
ret.typeName += t.Name()
break Loop
}
}
return ret
}
type arg struct {
pkg, typeName string
numPtr int
}
func (a *arg) normalizedPkg() string {
if a.pkg == "" {
return ""
}
// Strip the repo.../vendor/ prefix from the package path if present.
parts := strings.Split(a.pkg, "/")
// Remove vendor prefix.
for i := 0; i < len(parts); i++ {
if parts[i] == "vendor" {
parts = parts[i+1:]
break
}
}
switch strings.Join(parts, "/") {
case "google.golang.org/api/compute/v1":
return "ga."
case "google.golang.org/api/compute/v0.alpha":
return "alpha."
case "google.golang.org/api/compute/v0.beta":
return "beta."
default:
panic(fmt.Errorf("unhandled package %q", a.pkg))
}
}
func (a *arg) String() string {
var ret string
for i := 0; i < a.numPtr; i++ {
ret += "*"
}
ret += a.normalizedPkg()
ret += a.typeName
return ret
}
// newMethod returns a newly initialized method.
func newMethod(s *ServiceInfo, m reflect.Method) *Method {
ret := &Method{s, m, ""}
ret.init()
return ret
}
// Method is used to generate the calling code for non-standard methods.
type Method struct {
*ServiceInfo
m reflect.Method
ReturnType string
}
// argsSkip is the number of arguments to skip when generating the
// synthesized method.
func (mr *Method) argsSkip() int {
switch mr.keyType {
case Zonal:
return 4
case Regional:
return 4
case Global:
return 3
}
panic(fmt.Errorf("invalid KeyType %v", mr.keyType))
}
// args return a list of arguments to the method, skipping the first skip
// elements. If nameArgs is true, then the arguments will include a generated
// parameter name (arg<N>). prefix will be added to the parameters.
func (mr *Method) args(skip int, nameArgs bool, prefix []string) []string {
var args []*arg
fType := mr.m.Func.Type()
for i := 0; i < fType.NumIn(); i++ {
t := fType.In(i)
args = append(args, newArg(t))
}
var a []string
for i := skip; i < fType.NumIn(); i++ {
if nameArgs {
a = append(a, fmt.Sprintf("arg%d %s", i-skip, args[i]))
} else {
a = append(a, args[i].String())
}
}
return append(prefix, a...)
}
// init the method, preforming some rudimentary static checking.
func (mr *Method) init() {
fType := mr.m.Func.Type()
if fType.NumIn() < mr.argsSkip() {
err := fmt.Errorf("method %q.%q, arity = %d which is less than required (< %d)",
mr.Service, mr.Name(), fType.NumIn(), mr.argsSkip())
panic(err)
}
// Skipped args should all be string (they will be projectID, zone, region etc).
for i := 1; i < mr.argsSkip(); i++ {
if fType.In(i).Kind() != reflect.String {
panic(fmt.Errorf("method %q.%q: skipped args can only be strings", mr.Service, mr.Name()))
}
}
// Return of the method must return a single value of type *xxxCall.
if fType.NumOut() != 1 || fType.Out(0).Kind() != reflect.Ptr || !strings.HasSuffix(fType.Out(0).Elem().Name(), "Call") {
panic(fmt.Errorf("method %q.%q: generator only supports methods returning an *xxxCall object",
mr.Service, mr.Name()))
}
returnType := fType.Out(0)
returnTypeName := fType.Out(0).Elem().Name()
// xxxCall must have a Do() method.
doMethod, ok := returnType.MethodByName("Do")
if !ok {
panic(fmt.Errorf("method %q.%q: return type %q does not have a Do() method",
mr.Service, mr.Name(), returnTypeName))
}
// Do() method must return (*T, error).
switch doMethod.Func.Type().NumOut() {
case 2:
glog.Infof("Method %q.%q: return type %q of Do() = %v, %v",
mr.Service, mr.Name(), returnTypeName, doMethod.Func.Type().Out(0), doMethod.Func.Type().Out(1))
out0 := doMethod.Func.Type().Out(0)
if out0.Kind() != reflect.Ptr {
panic(fmt.Errorf("method %q.%q: return type %q of Do() = S, _; S must be pointer type (%v)",
mr.Service, mr.Name(), returnTypeName, out0))
}
mr.ReturnType = out0.Elem().Name()
if out0.Elem().Name() == "Operation" {
glog.Infof("Method %q.%q is an *Operation", mr.Service, mr.Name())
} else {
glog.Infof("Method %q.%q returns %v", mr.Service, mr.Name(), out0)
}
// Second argument must be "error".
if doMethod.Func.Type().Out(1).Name() != "error" {
panic(fmt.Errorf("method %q.%q: return type %q of Do() = S, T; T must be 'error'",
mr.Service, mr.Name(), returnTypeName))
}
break
default:
panic(fmt.Errorf("method %q.%q: %q Do() return type is not handled by the generator",
mr.Service, mr.Name(), returnTypeName))
}
}
// Name is the name of the method.
func (mr *Method) Name() string {
return mr.m.Name
}
// CallArgs is a list of comma separated "argN" used for calling the method.
// For example, if the method has two additional arguments, this will return
// "arg0, arg1".
func (mr *Method) CallArgs() string {
var args []string
for i := mr.argsSkip(); i < mr.m.Func.Type().NumIn(); i++ {
args = append(args, fmt.Sprintf("arg%d", i-mr.argsSkip()))
}
if len(args) == 0 {
return ""
}
return fmt.Sprintf(", %s", strings.Join(args, ", "))
}
// MockHookName is the name of the hook function in the mock.
func (mr *Method) MockHookName() string {
return mr.m.Name + "Hook"
}
// MockHook is the definition of the hook function.
func (mr *Method) MockHook() string {
args := mr.args(mr.argsSkip(), false, []string{
fmt.Sprintf("*%s", mr.MockWrapType()),
"context.Context",
"meta.Key",
})
if mr.ReturnType == "Operation" {
return fmt.Sprintf("%v func(%v) error", mr.MockHookName(), strings.Join(args, ", "))
}
return fmt.Sprintf("%v func(%v) (*%v.%v, error)", mr.MockHookName(), strings.Join(args, ", "), mr.Version(), mr.ReturnType)
}
// FcnArgs is the function signature for the definition of the method.
func (mr *Method) FcnArgs() string {
args := mr.args(mr.argsSkip(), true, []string{
"ctx context.Context",
"key meta.Key",
})
if mr.ReturnType == "Operation" {
return fmt.Sprintf("%v(%v) error", mr.m.Name, strings.Join(args, ", "))
}
return fmt.Sprintf("%v(%v) (*%v.%v, error)", mr.m.Name, strings.Join(args, ", "), mr.Version(), mr.ReturnType)
}
// InterfaceFunc is the function declaration of the method in the interface.
func (mr *Method) InterfaceFunc() string {
args := mr.args(mr.argsSkip(), false, []string{"context.Context", "meta.Key"})
if mr.ReturnType == "Operation" {
return fmt.Sprintf("%v(%v) error", mr.m.Name, strings.Join(args, ", "))
}
return fmt.Sprintf("%v(%v) (*%v.%v, error)", mr.m.Name, strings.Join(args, ", "), mr.Version(), mr.ReturnType)
}

View File

@ -0,0 +1,277 @@
/*
Copyright 2017 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.
*/
package meta
import (
"errors"
"fmt"
"reflect"
)
// ServiceInfo defines the entry for a Service that code will be generated for.
type ServiceInfo struct {
// Object is the Go name of the object type that the service deals
// with. Example: "ForwardingRule".
Object string
// Service is the Go name of the service struct i.e. where the methods
// are defined. Examples: "GlobalForwardingRules".
Service string
// Resource is the plural noun of the resource in the compute API URL (e.g.
// "forwardingRules").
Resource string
// version if unspecified will be assumed to be VersionGA.
version Version
keyType KeyType
serviceType reflect.Type
additionalMethods []string
options int
aggregatedListField string
}
// Version returns the version of the Service, defaulting to GA if APIVersion
// is empty.
func (i *ServiceInfo) Version() Version {
if i.version == "" {
return VersionGA
}
return i.version
}
// VersionTitle returns the capitalized golang CamelCase name for the version.
func (i *ServiceInfo) VersionTitle() string {
switch i.Version() {
case VersionGA:
return "GA"
case VersionAlpha:
return "Alpha"
case VersionBeta:
return "Beta"
}
panic(fmt.Errorf("invalid version %q", i.Version()))
}
// WrapType is the name of the wrapper service type.
func (i *ServiceInfo) WrapType() string {
switch i.Version() {
case VersionGA:
return i.Service
case VersionAlpha:
return "Alpha" + i.Service
case VersionBeta:
return "Beta" + i.Service
}
return "Invalid"
}
// WrapTypeOps is the name of the additional operations type.
func (i *ServiceInfo) WrapTypeOps() string {
return i.WrapType() + "Ops"
}
// FQObjectType is fully qualified name of the object (e.g. compute.Instance).
func (i *ServiceInfo) FQObjectType() string {
return fmt.Sprintf("%v.%v", i.Version(), i.Object)
}
// ObjectListType is the compute List type for the object (contains Items field).
func (i *ServiceInfo) ObjectListType() string {
return fmt.Sprintf("%v.%vList", i.Version(), i.Object)
}
// ObjectAggregatedListType is the compute List type for the object (contains Items field).
func (i *ServiceInfo) ObjectAggregatedListType() string {
return fmt.Sprintf("%v.%vAggregatedList", i.Version(), i.Object)
}
// MockWrapType is the name of the concrete mock for this type.
func (i *ServiceInfo) MockWrapType() string {
return "Mock" + i.WrapType()
}
// MockField is the name of the field in the mock struct.
func (i *ServiceInfo) MockField() string {
return "Mock" + i.WrapType()
}
// GCEWrapType is the name of the GCE wrapper type.
func (i *ServiceInfo) GCEWrapType() string {
return "GCE" + i.WrapType()
}
// Field is the name of the GCE struct.
func (i *ServiceInfo) Field() string {
return "gce" + i.WrapType()
}
// Methods returns a list of additional methods to generate code for.
func (i *ServiceInfo) Methods() []*Method {
methods := map[string]bool{}
for _, m := range i.additionalMethods {
methods[m] = true
}
var ret []*Method
for j := 0; j < i.serviceType.NumMethod(); j++ {
m := i.serviceType.Method(j)
if _, ok := methods[m.Name]; !ok {
continue
}
ret = append(ret, newMethod(i, m))
methods[m.Name] = false
}
for k, b := range methods {
if b {
panic(fmt.Errorf("method %q was not found in service %q", k, i.Service))
}
}
return ret
}
// KeyIsGlobal is true if the key is global.
func (i *ServiceInfo) KeyIsGlobal() bool {
return i.keyType == Global
}
// KeyIsRegional is true if the key is regional.
func (i *ServiceInfo) KeyIsRegional() bool {
return i.keyType == Regional
}
// KeyIsZonal is true if the key is zonal.
func (i *ServiceInfo) KeyIsZonal() bool {
return i.keyType == Zonal
}
// MakeKey returns the call used to create the appropriate key type.
func (i *ServiceInfo) MakeKey(name, location string) string {
switch i.keyType {
case Global:
return fmt.Sprintf("GlobalKey(%q)", name)
case Regional:
return fmt.Sprintf("RegionalKey(%q, %q)", name, location)
case Zonal:
return fmt.Sprintf("ZonalKey(%q, %q)", name, location)
}
return "Invalid"
}
// GenerateGet is true if the method is to be generated.
func (i *ServiceInfo) GenerateGet() bool {
return i.options&NoGet == 0
}
// GenerateList is true if the method is to be generated.
func (i *ServiceInfo) GenerateList() bool {
return i.options&NoList == 0
}
// GenerateDelete is true if the method is to be generated.
func (i *ServiceInfo) GenerateDelete() bool {
return i.options&NoDelete == 0
}
// GenerateInsert is true if the method is to be generated.
func (i *ServiceInfo) GenerateInsert() bool {
return i.options&NoInsert == 0
}
// GenerateCustomOps is true if we should generated a xxxOps interface for
// adding additional methods to the generated interface.
func (i *ServiceInfo) GenerateCustomOps() bool {
return i.options&CustomOps != 0
}
// AggregatedList is true if the method is to be generated.
func (i *ServiceInfo) AggregatedList() bool {
return i.options&AggregatedList != 0
}
// AggregatedListField is the name of the field used for the aggregated list
// call. This is typically the same as the name of the service, but can be
// customized by setting the aggregatedListField field.
func (i *ServiceInfo) AggregatedListField() string {
if i.aggregatedListField == "" {
return i.Service
}
return i.aggregatedListField
}
// ServiceGroup is a grouping of the same service but at different API versions.
type ServiceGroup struct {
Alpha *ServiceInfo
Beta *ServiceInfo
GA *ServiceInfo
}
// Service returns any ServiceInfo object belonging to the ServiceGroup.
func (sg *ServiceGroup) Service() string {
switch {
case sg.GA != nil:
return sg.GA.Service
case sg.Alpha != nil:
return sg.Alpha.Service
case sg.Beta != nil:
return sg.Beta.Service
default:
panic(errors.New("service group is empty"))
}
}
// HasGA returns true if this object has a GA representation.
func (sg *ServiceGroup) HasGA() bool {
return sg.GA != nil
}
// HasAlpha returns true if this object has a Alpha representation.
func (sg *ServiceGroup) HasAlpha() bool {
return sg.Alpha != nil
}
// HasBeta returns true if this object has a Beta representation.
func (sg *ServiceGroup) HasBeta() bool {
return sg.Beta != nil
}
// groupServices together by version.
func groupServices(services []*ServiceInfo) map[string]*ServiceGroup {
ret := map[string]*ServiceGroup{}
for _, si := range services {
if _, ok := ret[si.Service]; !ok {
ret[si.Service] = &ServiceGroup{}
}
group := ret[si.Service]
switch si.Version() {
case VersionAlpha:
group.Alpha = si
case VersionBeta:
group.Beta = si
case VersionGA:
group.GA = si
}
}
return ret
}
// AllServicesByGroup is a map of service name to ServicesGroup.
var AllServicesByGroup map[string]*ServiceGroup
func init() {
AllServicesByGroup = groupServices(AllServices)
}

View File

@ -0,0 +1,150 @@
/*
Copyright 2017 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.
*/
package cloud
import (
"context"
"reflect"
"testing"
alpha "google.golang.org/api/compute/v0.alpha"
beta "google.golang.org/api/compute/v0.beta"
ga "google.golang.org/api/compute/v1"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/filter"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/meta"
)
func TestMocks(t *testing.T) {
t.Parallel()
// This test uses Addresses, but the logic that is generated is the same for
// other basic objects.
const region = "us-central1"
ctx := context.Background()
mock := NewMockGCE()
keyAlpha := meta.RegionalKey("key-alpha", region)
keyBeta := meta.RegionalKey("key-beta", region)
keyGA := meta.RegionalKey("key-ga", region)
key := keyAlpha
// Get not found.
if _, err := mock.AlphaAddresses().Get(ctx, *key); err == nil {
t.Errorf("AlphaAddresses().Get(%v, %v) = _, nil; want error", ctx, key)
}
if _, err := mock.BetaAddresses().Get(ctx, *key); err == nil {
t.Errorf("BetaAddresses().Get(%v, %v) = _, nil; want error", ctx, key)
}
if _, err := mock.Addresses().Get(ctx, *key); err == nil {
t.Errorf("Addresses().Get(%v, %v) = _, nil; want error", ctx, key)
}
// Insert.
{
obj := &alpha.Address{}
if err := mock.AlphaAddresses().Insert(ctx, *keyAlpha, obj); err != nil {
t.Errorf("AlphaAddresses().Insert(%v, %v, %v) = %v; want nil", ctx, key, obj, err)
}
}
{
obj := &beta.Address{}
if err := mock.BetaAddresses().Insert(ctx, *keyBeta, obj); err != nil {
t.Errorf("BetaAddresses().Insert(%v, %v, %v) = %v; want nil", ctx, key, obj, err)
}
}
{
obj := &ga.Address{}
if err := mock.Addresses().Insert(ctx, *keyGA, &ga.Address{Name: "ga"}); err != nil {
t.Errorf("Addresses().Insert(%v, %v, %v) = %v; want nil", ctx, key, obj, err)
}
}
// Get across versions.
if obj, err := mock.AlphaAddresses().Get(ctx, *key); err != nil {
t.Errorf("AlphaAddresses().Get(%v, %v) = %v, %v; want nil", ctx, key, obj, err)
}
if obj, err := mock.BetaAddresses().Get(ctx, *key); err != nil {
t.Errorf("BetaAddresses().Get(%v, %v) = %v, %v; want nil", ctx, key, obj, err)
}
if obj, err := mock.Addresses().Get(ctx, *key); err != nil {
t.Errorf("Addresses().Get(%v, %v) = %v, %v; want nil", ctx, key, obj, err)
}
// List across versions.
want := map[string]bool{"key-alpha": true, "key-beta": true, "key-ga": true}
{
objs, err := mock.AlphaAddresses().List(ctx, region, filter.None)
if err != nil {
t.Errorf("AlphaAddresses().List(%v, %v, %v) = %v, %v; want _, nil", ctx, region, filter.None, objs, err)
} else {
got := map[string]bool{}
for _, obj := range objs {
got[obj.Name] = true
}
if !reflect.DeepEqual(got, want) {
t.Errorf("AlphaAddresses().List(); got %+v, want %+v", got, want)
}
}
}
{
objs, err := mock.BetaAddresses().List(ctx, region, filter.None)
if err != nil {
t.Errorf("BetaAddresses().List(%v, %v, %v) = %v, %v; want _, nil", ctx, region, filter.None, objs, err)
} else {
got := map[string]bool{}
for _, obj := range objs {
got[obj.Name] = true
}
if !reflect.DeepEqual(got, want) {
t.Errorf("AlphaAddresses().List(); got %+v, want %+v", got, want)
}
}
}
{
objs, err := mock.Addresses().List(ctx, region, filter.None)
if err != nil {
t.Errorf("Addresses().List(%v, %v, %v) = %v, %v; want _, nil", ctx, region, filter.None, objs, err)
} else {
got := map[string]bool{}
for _, obj := range objs {
got[obj.Name] = true
}
if !reflect.DeepEqual(got, want) {
t.Errorf("AlphaAddresses().List(); got %+v, want %+v", got, want)
}
}
}
// Delete across versions.
if err := mock.AlphaAddresses().Delete(ctx, *keyAlpha); err != nil {
t.Errorf("AlphaAddresses().Delete(%v, %v) = %v; want nil", ctx, key, err)
}
if err := mock.BetaAddresses().Delete(ctx, *keyBeta); err != nil {
t.Errorf("BetaAddresses().Delete(%v, %v) = %v; want nil", ctx, key, err)
}
if err := mock.Addresses().Delete(ctx, *keyGA); err != nil {
t.Errorf("Addresses().Delete(%v, %v) = %v; want nil", ctx, key, err)
}
// Delete not found.
if err := mock.AlphaAddresses().Delete(ctx, *keyAlpha); err == nil {
t.Errorf("AlphaAddresses().Delete(%v, %v) = nil; want error", ctx, key)
}
if err := mock.BetaAddresses().Delete(ctx, *keyBeta); err == nil {
t.Errorf("BetaAddresses().Delete(%v, %v) = nil; want error", ctx, key)
}
if err := mock.Addresses().Delete(ctx, *keyGA); err == nil {
t.Errorf("Addresses().Delete(%v, %v) = nil; want error", ctx, key)
}
}

View File

@ -0,0 +1,142 @@
/*
Copyright 2017 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.
*/
package cloud
import (
"context"
alpha "google.golang.org/api/compute/v0.alpha"
beta "google.golang.org/api/compute/v0.beta"
ga "google.golang.org/api/compute/v1"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/meta"
)
// operation is a GCE operation that can be watied on.
type operation interface {
// isDone queries GCE for the done status. This call can block.
isDone(ctx context.Context) (bool, error)
// rateLimitKey returns the rate limit key to use for the given operation.
// This rate limit will govern how fast the server will be polled for
// operation completion status.
rateLimitKey() *RateLimitKey
}
type gaOperation struct {
s *Service
op *ga.Operation
projectID string
}
func (o *gaOperation) isDone(ctx context.Context) (bool, error) {
var (
op *ga.Operation
err error
)
switch {
case o.op.Region != "":
op, err = o.s.GA.RegionOperations.Get(o.projectID, o.op.Region, o.op.Name).Context(ctx).Do()
case o.op.Zone != "":
op, err = o.s.GA.ZoneOperations.Get(o.projectID, o.op.Zone, o.op.Name).Context(ctx).Do()
default:
op, err = o.s.GA.GlobalOperations.Get(o.projectID, o.op.Name).Context(ctx).Do()
}
if err != nil {
return false, err
}
return op != nil && op.Status == "DONE", nil
}
func (o *gaOperation) rateLimitKey() *RateLimitKey {
return &RateLimitKey{
ProjectID: o.projectID,
Operation: "Get",
Service: "Operations",
Version: meta.VersionGA,
}
}
type alphaOperation struct {
s *Service
op *alpha.Operation
projectID string
}
func (o *alphaOperation) isDone(ctx context.Context) (bool, error) {
var (
op *alpha.Operation
err error
)
switch {
case o.op.Region != "":
op, err = o.s.Alpha.RegionOperations.Get(o.projectID, o.op.Region, o.op.Name).Context(ctx).Do()
case o.op.Zone != "":
op, err = o.s.Alpha.ZoneOperations.Get(o.projectID, o.op.Zone, o.op.Name).Context(ctx).Do()
default:
op, err = o.s.Alpha.GlobalOperations.Get(o.projectID, o.op.Name).Context(ctx).Do()
}
if err != nil {
return false, err
}
return op != nil && op.Status == "DONE", nil
}
func (o *alphaOperation) rateLimitKey() *RateLimitKey {
return &RateLimitKey{
ProjectID: o.projectID,
Operation: "Get",
Service: "Operations",
Version: meta.VersionAlpha,
}
}
type betaOperation struct {
s *Service
op *beta.Operation
projectID string
}
func (o *betaOperation) isDone(ctx context.Context) (bool, error) {
var (
op *beta.Operation
err error
)
switch {
case o.op.Region != "":
op, err = o.s.Beta.RegionOperations.Get(o.projectID, o.op.Region, o.op.Name).Context(ctx).Do()
case o.op.Zone != "":
op, err = o.s.Beta.ZoneOperations.Get(o.projectID, o.op.Zone, o.op.Name).Context(ctx).Do()
default:
op, err = o.s.Beta.GlobalOperations.Get(o.projectID, o.op.Name).Context(ctx).Do()
}
if err != nil {
return false, err
}
return op != nil && op.Status == "DONE", nil
}
func (o *betaOperation) rateLimitKey() *RateLimitKey {
return &RateLimitKey{
ProjectID: o.projectID,
Operation: "Get",
Service: "Operations",
Version: meta.VersionBeta,
}
}

View File

@ -0,0 +1,45 @@
/*
Copyright 2017 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.
*/
package cloud
import (
"context"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/meta"
)
// ProjectRouter routes service calls to the appropriate GCE project.
type ProjectRouter interface {
// ProjectID returns the project ID (non-numeric) to be used for a call
// to an API (version,service). Example tuples: ("ga", "ForwardingRules"),
// ("alpha", "GlobalAddresses").
//
// This allows for plumbing different service calls to the appropriate
// project, for instance, networking services to a separate project
// than instance management.
ProjectID(ctx context.Context, version meta.Version, service string) string
}
// SingleProjectRouter routes all service calls to the same project ID.
type SingleProjectRouter struct {
ID string
}
// ProjectID returns the project ID to be used for a call to the API.
func (r *SingleProjectRouter) ProjectID(ctx context.Context, version meta.Version, service string) string {
return r.ID
}

View File

@ -0,0 +1,68 @@
/*
Copyright 2017 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.
*/
package cloud
import (
"context"
"time"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/meta"
)
// RateLimitKey is a key identifying the operation to be rate limited. The rate limit
// queue will be determined based on the contents of RateKey.
type RateLimitKey struct {
// ProjectID is the non-numeric ID of the project.
ProjectID string
// Operation is the specific method being invoked (e.g. "Get", "List").
Operation string
// Version is the API version of the call.
Version meta.Version
// Service is the service being invoked (e.g. "Firewalls", "BackendServices")
Service string
}
// RateLimiter is the interface for a rate limiting policy.
type RateLimiter interface {
// Accept uses the RateLimitKey to derive a sleep time for the calling
// goroutine. This call will block until the operation is ready for
// execution.
//
// Accept returns an error if the given context ctx was canceled
// while waiting for acceptance into the queue.
Accept(ctx context.Context, key *RateLimitKey) error
}
// NopRateLimiter is a rate limiter that performs no rate limiting.
type NopRateLimiter struct {
}
// Accept the operation to be rate limited.
func (*NopRateLimiter) Accept(ctx context.Context, key *RateLimitKey) error {
// Rate limit polling of the Operation status to avoid hammering GCE
// for the status of an operation.
const pollTime = time.Duration(1) * time.Second
if key.Operation == "Get" && key.Service == "Operations" {
select {
case <-time.NewTimer(pollTime).C:
break
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}

View File

@ -0,0 +1,79 @@
/*
Copyright 2017 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.
*/
package cloud
import (
"context"
"fmt"
alpha "google.golang.org/api/compute/v0.alpha"
beta "google.golang.org/api/compute/v0.beta"
ga "google.golang.org/api/compute/v1"
)
// Service is the top-level adapter for all of the different compute API
// versions.
type Service struct {
GA *ga.Service
Alpha *alpha.Service
Beta *beta.Service
ProjectRouter ProjectRouter
RateLimiter RateLimiter
}
// wrapOperation wraps a GCE anyOP in a version generic operation type.
func (g *Service) wrapOperation(anyOp interface{}) (operation, error) {
switch o := anyOp.(type) {
case *ga.Operation:
r, err := ParseResourceURL(o.SelfLink)
if err != nil {
return nil, err
}
return &gaOperation{g, o, r.ProjectID}, nil
case *alpha.Operation:
r, err := ParseResourceURL(o.SelfLink)
if err != nil {
return nil, err
}
return &alphaOperation{g, o, r.ProjectID}, nil
case *beta.Operation:
r, err := ParseResourceURL(o.SelfLink)
if err != nil {
return nil, err
}
return &betaOperation{g, o, r.ProjectID}, nil
default:
return nil, fmt.Errorf("invalid type %T", anyOp)
}
}
// WaitForCompletion of a long running operation. This will poll the state of
// GCE for the completion status of the given operation. genericOp can be one
// of alpha, beta, ga Operation types.
func (g *Service) WaitForCompletion(ctx context.Context, genericOp interface{}) error {
op, err := g.wrapOperation(genericOp)
if err != nil {
return err
}
for done, err := op.isDone(ctx); !done; done, err = op.isDone(ctx) {
if err != nil {
return err
}
g.RateLimiter.Accept(ctx, op.rateLimitKey())
}
return nil
}

View File

@ -0,0 +1,167 @@
/*
Copyright 2017 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.
*/
package cloud
import (
"encoding/json"
"fmt"
"strings"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/meta"
)
const (
gaPrefix = "https://www.googleapis.com/compute/v1/"
alphaPrefix = "https://www.googleapis.com/compute/alpha/"
betaPrefix = "https://www.googleapis.com/compute/beta/"
)
var (
allPrefixes = []string{gaPrefix, alphaPrefix, betaPrefix}
)
// ResourceID identifies a GCE resource as parsed from compute resource URL.
type ResourceID struct {
ProjectID string
Resource string
Key *meta.Key
}
// Equal returns true if two resource IDs are equal.
func (r *ResourceID) Equal(other *ResourceID) bool {
if r.ProjectID != other.ProjectID || r.Resource != other.Resource {
return false
}
if r.Key != nil && other.Key != nil {
return *r.Key == *other.Key
}
if r.Key == nil && other.Key == nil {
return true
}
return false
}
// ParseResourceURL parses resource URLs of the following formats:
//
// projects/<proj>/global/<res>/<name>
// projects/<proj>/regions/<region>/<res>/<name>
// projects/<proj>/zones/<zone>/<res>/<name>
// [https://www.googleapis.com/compute/<ver>]/projects/<proj>/global/<res>/<name>
// [https://www.googleapis.com/compute/<ver>]/projects/<proj>/regions/<region>/<res>/<name>
// [https://www.googleapis.com/compute/<ver>]/projects/<proj>/zones/<zone>/<res>/<name>
func ParseResourceURL(url string) (*ResourceID, error) {
errNotValid := fmt.Errorf("%q is not a valid resource URL", url)
// Remove the "https://..." prefix if present
for _, prefix := range allPrefixes {
if strings.HasPrefix(url, prefix) {
if len(url) < len(prefix) {
return nil, errNotValid
}
url = url[len(prefix):]
break
}
}
parts := strings.Split(url, "/")
if len(parts) < 2 || parts[0] != "projects" {
return nil, errNotValid
}
ret := &ResourceID{ProjectID: parts[1]}
if len(parts) == 2 {
ret.Resource = "projects"
return ret, nil
}
if len(parts) < 4 {
return nil, errNotValid
}
if len(parts) == 4 {
switch parts[2] {
case "regions":
ret.Resource = "regions"
ret.Key = meta.GlobalKey(parts[3])
return ret, nil
case "zones":
ret.Resource = "zones"
ret.Key = meta.GlobalKey(parts[3])
return ret, nil
default:
return nil, errNotValid
}
}
switch parts[2] {
case "global":
if len(parts) != 5 {
return nil, errNotValid
}
ret.Resource = parts[3]
ret.Key = meta.GlobalKey(parts[4])
return ret, nil
case "regions":
if len(parts) != 6 {
return nil, errNotValid
}
ret.Resource = parts[4]
ret.Key = meta.RegionalKey(parts[5], parts[3])
return ret, nil
case "zones":
if len(parts) != 6 {
return nil, errNotValid
}
ret.Resource = parts[4]
ret.Key = meta.ZonalKey(parts[5], parts[3])
return ret, nil
}
return nil, errNotValid
}
func copyViaJSON(dest, src interface{}) error {
bytes, err := json.Marshal(src)
if err != nil {
return err
}
return json.Unmarshal(bytes, dest)
}
// SelfLink returns the self link URL for the given object.
func SelfLink(ver meta.Version, project, resource string, key meta.Key) string {
var prefix string
switch ver {
case meta.VersionAlpha:
prefix = alphaPrefix
case meta.VersionBeta:
prefix = betaPrefix
case meta.VersionGA:
prefix = gaPrefix
default:
prefix = "invalid-prefix"
}
switch key.Type() {
case meta.Zonal:
return fmt.Sprintf("%sprojects/%s/zones/%s/%s/%s", prefix, project, key.Zone, resource, key.Name)
case meta.Regional:
return fmt.Sprintf("%sprojects/%s/regions/%s/%s/%s", prefix, project, key.Region, resource, key.Name)
case meta.Global:
return fmt.Sprintf("%sprojects/%s/%s/%s", prefix, project, resource, key.Name)
}
return "invalid-self-link"
}

View File

@ -0,0 +1,197 @@
/*
Copyright 2017 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.
*/
package cloud
import (
"errors"
"testing"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce/cloud/meta"
)
func TestParseResourceURL(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
in string
r *ResourceID
}{
{
"https://www.googleapis.com/compute/v1/projects/some-gce-project",
&ResourceID{"some-gce-project", "projects", nil},
},
{
"https://www.googleapis.com/compute/v1/projects/some-gce-project/regions/us-central1",
&ResourceID{"some-gce-project", "regions", meta.GlobalKey("us-central1")},
},
{
"https://www.googleapis.com/compute/v1/projects/some-gce-project/zones/us-central1-b",
&ResourceID{"some-gce-project", "zones", meta.GlobalKey("us-central1-b")},
},
{
"https://www.googleapis.com/compute/v1/projects/some-gce-project/global/operations/operation-1513289952196-56054460af5a0-b1dae0c3-9bbf9dbf",
&ResourceID{"some-gce-project", "operations", meta.GlobalKey("operation-1513289952196-56054460af5a0-b1dae0c3-9bbf9dbf")},
},
{
"https://www.googleapis.com/compute/alpha/projects/some-gce-project/regions/us-central1/addresses/my-address",
&ResourceID{"some-gce-project", "addresses", meta.RegionalKey("my-address", "us-central1")},
},
{
"https://www.googleapis.com/compute/v1/projects/some-gce-project/zones/us-central1-c/instances/instance-1",
&ResourceID{"some-gce-project", "instances", meta.ZonalKey("instance-1", "us-central1-c")},
},
{
"projects/some-gce-project",
&ResourceID{"some-gce-project", "projects", nil},
},
{
"projects/some-gce-project/regions/us-central1",
&ResourceID{"some-gce-project", "regions", meta.GlobalKey("us-central1")},
},
{
"projects/some-gce-project/zones/us-central1-b",
&ResourceID{"some-gce-project", "zones", meta.GlobalKey("us-central1-b")},
},
{
"projects/some-gce-project/global/operations/operation-1513289952196-56054460af5a0-b1dae0c3-9bbf9dbf",
&ResourceID{"some-gce-project", "operations", meta.GlobalKey("operation-1513289952196-56054460af5a0-b1dae0c3-9bbf9dbf")},
},
{
"projects/some-gce-project/regions/us-central1/addresses/my-address",
&ResourceID{"some-gce-project", "addresses", meta.RegionalKey("my-address", "us-central1")},
},
{
"projects/some-gce-project/zones/us-central1-c/instances/instance-1",
&ResourceID{"some-gce-project", "instances", meta.ZonalKey("instance-1", "us-central1-c")},
},
} {
r, err := ParseResourceURL(tc.in)
if err != nil {
t.Errorf("ParseResourceURL(%q) = %+v, %v; want _, nil", tc.in, r, err)
continue
}
if !r.Equal(tc.r) {
t.Errorf("ParseResourceURL(%q) = %+v, nil; want %+v, nil", tc.in, r, tc.r)
}
}
// Malformed URLs.
for _, tc := range []string{
"",
"/",
"/a",
"/a/b",
"/a/b/c",
"/a/b/c/d",
"/a/b/c/d/e",
"/a/b/c/d/e/f",
"https://www.googleapis.com/compute/v1/projects/some-gce-project/global",
"projects/some-gce-project/global",
"projects/some-gce-project/global/foo/bar/baz",
"projects/some-gce-project/zones/us-central1-c/res",
"projects/some-gce-project/zones/us-central1-c/res/name/extra",
"https://www.googleapis.com/compute/gamma/projects/some-gce-project/global/addresses/name",
} {
r, err := ParseResourceURL(tc)
if err == nil {
t.Errorf("ParseResourceURL(%q) = %+v, %v, want _, error", tc, r, err)
}
}
}
type A struct {
A, B, C string
}
type B struct {
A, B, D string
}
type E struct{}
func (*E) MarshalJSON() ([]byte, error) {
return nil, errors.New("injected error")
}
func TestCopyVisJSON(t *testing.T) {
t.Parallel()
var b B
srcA := &A{"aa", "bb", "cc"}
err := copyViaJSON(&b, srcA)
if err != nil {
t.Errorf(`copyViaJSON(&b, %+v) = %v, want nil`, srcA, err)
} else {
expectedB := B{"aa", "bb", ""}
if b != expectedB {
t.Errorf("b == %+v, want %+v", b, expectedB)
}
}
var a A
srcB := &B{"aaa", "bbb", "ccc"}
err = copyViaJSON(&a, srcB)
if err != nil {
t.Errorf(`copyViaJSON(&a, %+v) = %v, want nil`, srcB, err)
} else {
expectedA := A{"aaa", "bbb", ""}
if a != expectedA {
t.Errorf("a == %+v, want %+v", a, expectedA)
}
}
if err := copyViaJSON(&a, &E{}); err == nil {
t.Errorf("copyViaJSON(&a, &E{}) = nil, want error")
}
}
func TestSelfLink(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
ver meta.Version
project string
resource string
key meta.Key
want string
}{
{
meta.VersionAlpha,
"proj1",
"addresses",
*meta.RegionalKey("key1", "us-central1"),
"https://www.googleapis.com/compute/alpha/projects/proj1/regions/us-central1/addresses/key1",
},
{
meta.VersionBeta,
"proj3",
"disks",
*meta.ZonalKey("key2", "us-central1-b"),
"https://www.googleapis.com/compute/beta/projects/proj3/zones/us-central1-b/disks/key2",
},
{
meta.VersionGA,
"proj4",
"urlMaps",
*meta.GlobalKey("key3"),
"https://www.googleapis.com/compute/v1/projects/proj4/urlMaps/key3",
},
} {
if link := SelfLink(tc.ver, tc.project, tc.resource, tc.key); link != tc.want {
t.Errorf("SelfLink(%v, %q, %q, %v) = %v, want %q", tc.ver, tc.project, tc.resource, tc.key, link, tc.want)
}
}
}