mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 09:22:44 +00:00
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:
commit
b873fc4453
@ -87,6 +87,7 @@ pkg/cloudprovider
|
|||||||
pkg/cloudprovider/providers/aws
|
pkg/cloudprovider/providers/aws
|
||||||
pkg/cloudprovider/providers/fake
|
pkg/cloudprovider/providers/fake
|
||||||
pkg/cloudprovider/providers/gce
|
pkg/cloudprovider/providers/gce
|
||||||
|
pkg/cloudprovider/providers/gce/cloud
|
||||||
pkg/cloudprovider/providers/openstack
|
pkg/cloudprovider/providers/openstack
|
||||||
pkg/cloudprovider/providers/ovirt
|
pkg/cloudprovider/providers/ovirt
|
||||||
pkg/cloudprovider/providers/photon
|
pkg/cloudprovider/providers/photon
|
||||||
|
38
hack/update-cloudprovider-gce.sh
Executable file
38
hack/update-cloudprovider-gce.sh
Executable 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
|
49
hack/verify-cloudprovider-gce.sh
Executable file
49
hack/verify-cloudprovider-gce.sh
Executable 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
|
@ -125,6 +125,9 @@ filegroup(
|
|||||||
|
|
||||||
filegroup(
|
filegroup(
|
||||||
name = "all-srcs",
|
name = "all-srcs",
|
||||||
srcs = [":package-srcs"],
|
srcs = [
|
||||||
|
":package-srcs",
|
||||||
|
"//pkg/cloudprovider/providers/gce/cloud:all-srcs",
|
||||||
|
],
|
||||||
tags = ["automanaged"],
|
tags = ["automanaged"],
|
||||||
)
|
)
|
||||||
|
63
pkg/cloudprovider/providers/gce/cloud/BUILD
Normal file
63
pkg/cloudprovider/providers/gce/cloud/BUILD
Normal 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"],
|
||||||
|
)
|
112
pkg/cloudprovider/providers/gce/cloud/doc.go
Normal file
112
pkg/cloudprovider/providers/gce/cloud/doc.go
Normal 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
|
30
pkg/cloudprovider/providers/gce/cloud/filter/BUILD
Normal file
30
pkg/cloudprovider/providers/gce/cloud/filter/BUILD
Normal 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"],
|
||||||
|
)
|
303
pkg/cloudprovider/providers/gce/cloud/filter/filter.go
Normal file
303
pkg/cloudprovider/providers/gce/cloud/filter/filter.go
Normal 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)
|
||||||
|
}
|
176
pkg/cloudprovider/providers/gce/cloud/filter/filter_test.go
Normal file
176
pkg/cloudprovider/providers/gce/cloud/filter/filter_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
99
pkg/cloudprovider/providers/gce/cloud/gce_projects.go
Normal file
99
pkg/cloudprovider/providers/gce/cloud/gce_projects.go
Normal 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)
|
||||||
|
}
|
10446
pkg/cloudprovider/providers/gce/cloud/gen.go
Normal file
10446
pkg/cloudprovider/providers/gce/cloud/gen.go
Normal file
File diff suppressed because it is too large
Load Diff
30
pkg/cloudprovider/providers/gce/cloud/gen/BUILD
Normal file
30
pkg/cloudprovider/providers/gce/cloud/gen/BUILD
Normal 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"],
|
||||||
|
)
|
1150
pkg/cloudprovider/providers/gce/cloud/gen/main.go
Normal file
1150
pkg/cloudprovider/providers/gce/cloud/gen/main.go
Normal file
File diff suppressed because it is too large
Load Diff
1749
pkg/cloudprovider/providers/gce/cloud/gen_test.go
Normal file
1749
pkg/cloudprovider/providers/gce/cloud/gen_test.go
Normal file
File diff suppressed because it is too large
Load Diff
41
pkg/cloudprovider/providers/gce/cloud/meta/BUILD
Normal file
41
pkg/cloudprovider/providers/gce/cloud/meta/BUILD
Normal 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"],
|
||||||
|
)
|
19
pkg/cloudprovider/providers/gce/cloud/meta/doc.go
Normal file
19
pkg/cloudprovider/providers/gce/cloud/meta/doc.go
Normal 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
|
96
pkg/cloudprovider/providers/gce/cloud/meta/key.go
Normal file
96
pkg/cloudprovider/providers/gce/cloud/meta/key.go
Normal 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
|
||||||
|
}
|
75
pkg/cloudprovider/providers/gce/cloud/meta/key_test.go
Normal file
75
pkg/cloudprovider/providers/gce/cloud/meta/key_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
372
pkg/cloudprovider/providers/gce/cloud/meta/meta.go
Normal file
372
pkg/cloudprovider/providers/gce/cloud/meta/meta.go
Normal 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{}),
|
||||||
|
},
|
||||||
|
}
|
250
pkg/cloudprovider/providers/gce/cloud/meta/method.go
Normal file
250
pkg/cloudprovider/providers/gce/cloud/meta/method.go
Normal 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)
|
||||||
|
}
|
277
pkg/cloudprovider/providers/gce/cloud/meta/service.go
Normal file
277
pkg/cloudprovider/providers/gce/cloud/meta/service.go
Normal 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)
|
||||||
|
}
|
150
pkg/cloudprovider/providers/gce/cloud/mock_test.go
Normal file
150
pkg/cloudprovider/providers/gce/cloud/mock_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
142
pkg/cloudprovider/providers/gce/cloud/op.go
Normal file
142
pkg/cloudprovider/providers/gce/cloud/op.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
45
pkg/cloudprovider/providers/gce/cloud/project.go
Normal file
45
pkg/cloudprovider/providers/gce/cloud/project.go
Normal 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
|
||||||
|
}
|
68
pkg/cloudprovider/providers/gce/cloud/ratelimit.go
Normal file
68
pkg/cloudprovider/providers/gce/cloud/ratelimit.go
Normal 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
|
||||||
|
}
|
79
pkg/cloudprovider/providers/gce/cloud/service.go
Normal file
79
pkg/cloudprovider/providers/gce/cloud/service.go
Normal 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
|
||||||
|
}
|
167
pkg/cloudprovider/providers/gce/cloud/utils.go
Normal file
167
pkg/cloudprovider/providers/gce/cloud/utils.go
Normal 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"
|
||||||
|
}
|
197
pkg/cloudprovider/providers/gce/cloud/utils_test.go
Normal file
197
pkg/cloudprovider/providers/gce/cloud/utils_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user