diff --git a/pkg/cloudprovider/providers/gce/BUILD b/pkg/cloudprovider/providers/gce/BUILD index f33f482e6e..0952ba724c 100644 --- a/pkg/cloudprovider/providers/gce/BUILD +++ b/pkg/cloudprovider/providers/gce/BUILD @@ -13,6 +13,7 @@ go_library( "gce.go", "gce_addresses.go", "gce_addresses_fakes.go", + "gce_alpha.go", "gce_annotations.go", "gce_backendservice.go", "gce_cert.go", diff --git a/pkg/cloudprovider/providers/gce/gce.go b/pkg/cloudprovider/providers/gce/gce.go index e9f78a83c5..aa03e5afb5 100644 --- a/pkg/cloudprovider/providers/gce/gce.go +++ b/pkg/cloudprovider/providers/gce/gce.go @@ -118,6 +118,11 @@ type GCECloud struct { // lock to prevent shared resources from being prematurely deleted while the operation is // in progress. sharedResourceLock sync.Mutex + // AlphaFeatureGate gates gce alpha features in GCECloud instance. + // Related wrapper functions that interacts with gce alpha api should examine whether + // the corresponding api is enabled. + // If not enabled, it should return error. + AlphaFeatureGate *AlphaFeatureGate } type ServiceManager interface { @@ -151,6 +156,9 @@ type ConfigFile struct { // Specifying ApiEndpoint will override the default GCE compute API endpoint. ApiEndpoint string `gcfg:"api-endpoint"` LocalZone string `gcfg:"local-zone"` + // Possible values: List of api names separated by comma. Default to none. + // For example: MyFeatureFlag + AlphaFeatures []string `gcfg:"alpha-features"` } } @@ -167,6 +175,7 @@ type CloudConfig struct { NodeInstancePrefix string TokenSource oauth2.TokenSource UseMetadataServer bool + AlphaFeatureGate *AlphaFeatureGate } func init() { @@ -245,6 +254,12 @@ func generateCloudConfig(configFile *ConfigFile) (cloudConfig *CloudConfig, err cloudConfig.NodeTags = configFile.Global.NodeTags cloudConfig.NodeInstancePrefix = configFile.Global.NodeInstancePrefix + + alphaFeatureGate, err := NewAlphaFeatureGate(configFile.Global.AlphaFeatures) + if err != nil { + glog.Errorf("Encountered error for creating alpha feature gate: %v", err) + } + cloudConfig.AlphaFeatureGate = alphaFeatureGate } // retrieve projectID and zone @@ -398,6 +413,7 @@ func CreateGCECloud(config *CloudConfig) (*GCECloud, error) { nodeInstancePrefix: config.NodeInstancePrefix, useMetadataServer: config.UseMetadataServer, operationPollRateLimiter: operationPollRateLimiter, + AlphaFeatureGate: config.AlphaFeatureGate, } gce.manager = &GCEServiceManager{gce} diff --git a/pkg/cloudprovider/providers/gce/gce_alpha.go b/pkg/cloudprovider/providers/gce/gce_alpha.go new file mode 100644 index 0000000000..fb659c9391 --- /dev/null +++ b/pkg/cloudprovider/providers/gce/gce_alpha.go @@ -0,0 +1,47 @@ +/* +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 gce + +import ( + "fmt" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" +) + +// All known alpha features +var knownAlphaFeatures = map[string]bool{} + +type AlphaFeatureGate struct { + features map[string]bool +} + +func (af *AlphaFeatureGate) Enabled(key string) bool { + return af.features[key] +} + +func NewAlphaFeatureGate(features []string) (*AlphaFeatureGate, error) { + errList := []error{} + featureMap := make(map[string]bool) + for _, name := range features { + if _, ok := knownAlphaFeatures[name]; !ok { + errList = append(errList, fmt.Errorf("alpha feature %q is not supported.", name)) + } else { + featureMap[name] = true + } + } + return &AlphaFeatureGate{featureMap}, utilerrors.NewAggregate(errList) +} diff --git a/pkg/cloudprovider/providers/gce/gce_test.go b/pkg/cloudprovider/providers/gce/gce_test.go index da70b1246f..f3add41c49 100644 --- a/pkg/cloudprovider/providers/gce/gce_test.go +++ b/pkg/cloudprovider/providers/gce/gce_test.go @@ -262,6 +262,36 @@ func TestSplitProviderID(t *testing.T) { } } +type generateConfigParams struct { + TokenURL string + TokenBody string + ProjectID string + NetworkName string + SubnetworkName string + NodeTags []string + NodeInstancePrefix string + Multizone bool + ApiEndpoint string + LocalZone string + AlphaFeatures []string +} + +func newGenerateConfigDefaults() *generateConfigParams { + return &generateConfigParams{ + TokenURL: "", + TokenBody: "", + ProjectID: "project-id", + NetworkName: "network-name", + SubnetworkName: "", + NodeTags: []string{"node-tag"}, + NodeInstancePrefix: "node-prefix", + Multizone: false, + ApiEndpoint: "", + LocalZone: "us-central1-a", + AlphaFeatures: []string{}, + } +} + func TestGenerateCloudConfigs(t *testing.T) { testCases := []struct { TokenURL string @@ -275,96 +305,10 @@ func TestGenerateCloudConfigs(t *testing.T) { ApiEndpoint string LocalZone string cloudConfig *CloudConfig + AlphaFeatures []string }{ + // default config { - TokenURL: "", - TokenBody: "", - ProjectID: "project-id", - NetworkName: "network-name", - SubnetworkName: "subnetwork-name", - NodeTags: []string{"node-tag"}, - NodeInstancePrefix: "node-prefix", - Multizone: false, - ApiEndpoint: "", - LocalZone: "us-central1-a", - cloudConfig: &CloudConfig{ - ApiEndpoint: "", - ProjectID: "project-id", - Region: "us-central1", - Zone: "us-central1-a", - ManagedZones: []string{"us-central1-a"}, - NetworkURL: "https://www.googleapis.com/compute/v1/projects/project-id/global/networks/network-name", - SubnetworkURL: "https://www.googleapis.com/compute/v1/projects/project-id/regions/us-central1/subnetworks/subnetwork-name", - NodeTags: []string{"node-tag"}, - NodeInstancePrefix: "node-prefix", - TokenSource: google.ComputeTokenSource(""), - UseMetadataServer: true, - }, - }, - // nil token source - { - TokenURL: "nil", - TokenBody: "", - ProjectID: "project-id", - NetworkName: "network-name", - SubnetworkName: "subnetwork-name", - NodeTags: []string{"node-tag"}, - NodeInstancePrefix: "node-prefix", - Multizone: false, - ApiEndpoint: "", - LocalZone: "us-central1-a", - cloudConfig: &CloudConfig{ - ApiEndpoint: "", - ProjectID: "project-id", - Region: "us-central1", - Zone: "us-central1-a", - ManagedZones: []string{"us-central1-a"}, - NetworkURL: "https://www.googleapis.com/compute/v1/projects/project-id/global/networks/network-name", - SubnetworkURL: "https://www.googleapis.com/compute/v1/projects/project-id/regions/us-central1/subnetworks/subnetwork-name", - NodeTags: []string{"node-tag"}, - NodeInstancePrefix: "node-prefix", - TokenSource: nil, - UseMetadataServer: true, - }, - }, - // specified api endpoint - { - TokenURL: "", - TokenBody: "", - ProjectID: "project-id", - NetworkName: "network-name", - SubnetworkName: "subnetwork-name", - NodeTags: []string{"node-tag"}, - NodeInstancePrefix: "node-prefix", - Multizone: false, - ApiEndpoint: "https://www.googleapis.com/compute/staging_v1/", - LocalZone: "us-central1-a", - cloudConfig: &CloudConfig{ - ApiEndpoint: "https://www.googleapis.com/compute/staging_v1/", - ProjectID: "project-id", - Region: "us-central1", - Zone: "us-central1-a", - ManagedZones: []string{"us-central1-a"}, - NetworkURL: "https://www.googleapis.com/compute/staging_v1/projects/project-id/global/networks/network-name", - SubnetworkURL: "https://www.googleapis.com/compute/staging_v1/projects/project-id/regions/us-central1/subnetworks/subnetwork-name", - NodeTags: []string{"node-tag"}, - NodeInstancePrefix: "node-prefix", - TokenSource: google.ComputeTokenSource(""), - UseMetadataServer: true, - }, - }, - // empty subnet-name - { - TokenURL: "", - TokenBody: "", - ProjectID: "project-id", - NetworkName: "network-name", - SubnetworkName: "", - NodeTags: []string{"node-tag"}, - NodeInstancePrefix: "node-prefix", - Multizone: false, - ApiEndpoint: "", - LocalZone: "us-central1-a", cloudConfig: &CloudConfig{ ApiEndpoint: "", ProjectID: "project-id", @@ -377,20 +321,84 @@ func TestGenerateCloudConfigs(t *testing.T) { NodeInstancePrefix: "node-prefix", TokenSource: google.ComputeTokenSource(""), UseMetadataServer: true, + AlphaFeatureGate: &AlphaFeatureGate{map[string]bool{}}, + }, + }, + // nil token source + { + TokenURL: "nil", + cloudConfig: &CloudConfig{ + ApiEndpoint: "", + ProjectID: "project-id", + Region: "us-central1", + Zone: "us-central1-a", + ManagedZones: []string{"us-central1-a"}, + NetworkURL: "https://www.googleapis.com/compute/v1/projects/project-id/global/networks/network-name", + SubnetworkURL: "", + NodeTags: []string{"node-tag"}, + NodeInstancePrefix: "node-prefix", + TokenSource: nil, + UseMetadataServer: true, + AlphaFeatureGate: &AlphaFeatureGate{map[string]bool{}}, + }, + }, + // specified api endpoint + { + ApiEndpoint: "https://www.googleapis.com/compute/staging_v1/", + cloudConfig: &CloudConfig{ + ApiEndpoint: "https://www.googleapis.com/compute/staging_v1/", + ProjectID: "project-id", + Region: "us-central1", + Zone: "us-central1-a", + ManagedZones: []string{"us-central1-a"}, + NetworkURL: "https://www.googleapis.com/compute/staging_v1/projects/project-id/global/networks/network-name", + SubnetworkURL: "", + NodeTags: []string{"node-tag"}, + NodeInstancePrefix: "node-prefix", + TokenSource: google.ComputeTokenSource(""), + UseMetadataServer: true, + AlphaFeatureGate: &AlphaFeatureGate{map[string]bool{}}, + }, + }, + // fqdn subnetname + { + SubnetworkName: "https://www.googleapis.com/compute/v1/projects/project-id/regions/us-central1/subnetworks/subnetwork-name", + cloudConfig: &CloudConfig{ + ApiEndpoint: "", + ProjectID: "project-id", + Region: "us-central1", + Zone: "us-central1-a", + ManagedZones: []string{"us-central1-a"}, + NetworkURL: "https://www.googleapis.com/compute/v1/projects/project-id/global/networks/network-name", + SubnetworkURL: "https://www.googleapis.com/compute/v1/projects/project-id/regions/us-central1/subnetworks/subnetwork-name", + NodeTags: []string{"node-tag"}, + NodeInstancePrefix: "node-prefix", + TokenSource: google.ComputeTokenSource(""), + UseMetadataServer: true, + AlphaFeatureGate: &AlphaFeatureGate{map[string]bool{}}, + }, + }, + // subnetname + { + SubnetworkName: "subnetwork-name", + cloudConfig: &CloudConfig{ + ApiEndpoint: "", + ProjectID: "project-id", + Region: "us-central1", + Zone: "us-central1-a", + ManagedZones: []string{"us-central1-a"}, + NetworkURL: "https://www.googleapis.com/compute/v1/projects/project-id/global/networks/network-name", + SubnetworkURL: "https://www.googleapis.com/compute/v1/projects/project-id/regions/us-central1/subnetworks/subnetwork-name", + NodeTags: []string{"node-tag"}, + NodeInstancePrefix: "node-prefix", + TokenSource: google.ComputeTokenSource(""), + UseMetadataServer: true, + AlphaFeatureGate: &AlphaFeatureGate{map[string]bool{}}, }, }, // multi zone { - TokenURL: "", - TokenBody: "", - ProjectID: "project-id", - NetworkName: "network-name", - SubnetworkName: "subnetwork-name", - NodeTags: []string{"node-tag"}, - NodeInstancePrefix: "node-prefix", - Multizone: true, - ApiEndpoint: "", - LocalZone: "us-central1-a", + Multizone: true, cloudConfig: &CloudConfig{ ApiEndpoint: "", ProjectID: "project-id", @@ -398,16 +406,45 @@ func TestGenerateCloudConfigs(t *testing.T) { Zone: "us-central1-a", ManagedZones: nil, NetworkURL: "https://www.googleapis.com/compute/v1/projects/project-id/global/networks/network-name", - SubnetworkURL: "https://www.googleapis.com/compute/v1/projects/project-id/regions/us-central1/subnetworks/subnetwork-name", + SubnetworkURL: "", NodeTags: []string{"node-tag"}, NodeInstancePrefix: "node-prefix", TokenSource: google.ComputeTokenSource(""), UseMetadataServer: true, + AlphaFeatureGate: &AlphaFeatureGate{map[string]bool{}}, }, }, } for _, tc := range testCases { + config := newGenerateConfigDefaults() + config.Multizone = tc.Multizone + config.ApiEndpoint = tc.ApiEndpoint + config.AlphaFeatures = tc.AlphaFeatures + config.TokenBody = tc.TokenBody + + if tc.TokenURL != "" { + config.TokenURL = tc.TokenURL + } + if tc.ProjectID != "" { + config.ProjectID = tc.ProjectID + } + if tc.NetworkName != "" { + config.NetworkName = tc.NetworkName + } + if tc.SubnetworkName != "" { + config.SubnetworkName = tc.SubnetworkName + } + if len(tc.NodeTags) > 0 { + config.NodeTags = tc.NodeTags + } + if tc.NodeInstancePrefix != "" { + config.NodeInstancePrefix = tc.NodeInstancePrefix + } + if tc.LocalZone != "" { + config.LocalZone = tc.LocalZone + } + cloudConfig, err := generateCloudConfig(&ConfigFile{ Global: struct { TokenURL string `gcfg:"token-url"` @@ -420,17 +457,19 @@ func TestGenerateCloudConfigs(t *testing.T) { Multizone bool `gcfg:"multizone"` ApiEndpoint string `gcfg:"api-endpoint"` LocalZone string `gcfg:"local-zone"` + AlphaFeatures []string `gcfg:"alpha-features"` }{ - TokenURL: tc.TokenURL, - TokenBody: tc.TokenBody, - ProjectID: tc.ProjectID, - NetworkName: tc.NetworkName, - SubnetworkName: tc.SubnetworkName, - NodeTags: tc.NodeTags, - NodeInstancePrefix: tc.NodeInstancePrefix, - Multizone: tc.Multizone, - ApiEndpoint: tc.ApiEndpoint, - LocalZone: tc.LocalZone, + TokenURL: config.TokenURL, + TokenBody: config.TokenBody, + ProjectID: config.ProjectID, + NetworkName: config.NetworkName, + SubnetworkName: config.SubnetworkName, + NodeTags: config.NodeTags, + NodeInstancePrefix: config.NodeInstancePrefix, + Multizone: config.Multizone, + ApiEndpoint: config.ApiEndpoint, + LocalZone: config.LocalZone, + AlphaFeatures: config.AlphaFeatures, }, }) if err != nil { @@ -492,3 +531,65 @@ func getTestOperation() *computev1.Operation { }, } } + +func TestNewAlphaFeatureGate(t *testing.T) { + knownAlphaFeatures["foo"] = true + knownAlphaFeatures["bar"] = true + + testCases := []struct { + alphaFeatures []string + expectEnabled []string + expectDisabled []string + expectError bool + }{ + // enable foo bar + { + alphaFeatures: []string{"foo", "bar"}, + expectEnabled: []string{"foo", "bar"}, + expectDisabled: []string{"aaa"}, + expectError: false, + }, + // no alpha feature + { + alphaFeatures: []string{}, + expectEnabled: []string{}, + expectDisabled: []string{"foo", "bar"}, + expectError: false, + }, + // unsupported alpha feature + { + alphaFeatures: []string{"aaa", "foo"}, + expectError: true, + expectEnabled: []string{"foo"}, + expectDisabled: []string{"aaa"}, + }, + // enable foo + { + alphaFeatures: []string{"foo"}, + expectEnabled: []string{"foo"}, + expectDisabled: []string{"bar"}, + expectError: false, + }, + } + + for _, tc := range testCases { + featureGate, err := NewAlphaFeatureGate(tc.alphaFeatures) + + if (tc.expectError && err == nil) || (!tc.expectError && err != nil) { + t.Errorf("Expect error to be %v, but got error %v", tc.expectError, err) + } + + for _, key := range tc.expectEnabled { + if !featureGate.Enabled(key) { + t.Errorf("Expect %q to be enabled.", key) + } + } + for _, key := range tc.expectDisabled { + if featureGate.Enabled(key) { + t.Errorf("Expect %q to be disabled.", key) + } + } + } + delete(knownAlphaFeatures, "foo") + delete(knownAlphaFeatures, "bar") +}