Add glide.yaml and vendor deps

This commit is contained in:
Dalton Hubble 2016-12-03 22:43:32 -08:00
parent db918f12ad
commit 5b3d5e81bd
18880 changed files with 5166045 additions and 1 deletions

View file

@ -0,0 +1,33 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = [
"dns.go",
"doc.go",
"plugins.go",
],
tags = ["automanaged"],
deps = [
"//federation/pkg/dnsprovider/rrstype:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = ["dns_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = ["//federation/pkg/dnsprovider/rrstype:go_default_library"],
)

View file

@ -0,0 +1,98 @@
/*
Copyright 2016 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 dnsprovider
import (
"reflect"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
)
// Interface is an abstract, pluggable interface for DNS providers.
type Interface interface {
// Zones returns the provider's Zones interface, or false if not supported.
Zones() (Zones, bool)
}
type Zones interface {
// List returns the managed Zones, or an error if the list operation failed.
List() ([]Zone, error)
// Add creates and returns a new managed zone, or an error if the operation failed
Add(Zone) (Zone, error)
// Remove deletes a managed zone, or returns an error if the operation failed.
Remove(Zone) error
// New allocates a new Zone, which can then be passed to Add()
// Arguments are as per the Zone interface below.
New(name string) (Zone, error)
}
type Zone interface {
// Name returns the name of the zone, e.g. "example.com"
Name() string
// ID returns the unique provider identifier for the zone
ID() string
// ResourceRecordsets returns the provider's ResourceRecordSets interface, or false if not supported.
ResourceRecordSets() (ResourceRecordSets, bool)
}
type ResourceRecordSets interface {
// List returns the ResourceRecordSets of the Zone, or an error if the list operation failed.
List() ([]ResourceRecordSet, error)
// New allocates a new ResourceRecordSet, which can then be passed to ResourceRecordChangeset Add() or Remove()
// Arguments are as per the ResourceRecordSet interface below.
New(name string, rrdatas []string, ttl int64, rrstype rrstype.RrsType) ResourceRecordSet
// StartChangeset begins a new batch operation of changes against the Zone
StartChangeset() ResourceRecordChangeset
}
// ResourceRecordChangeset accumulates a set of changes, that can then be applied with Apply
type ResourceRecordChangeset interface {
// Add adds the creation of a ResourceRecordSet in the Zone to the changeset
Add(ResourceRecordSet) ResourceRecordChangeset
// Remove adds the removal of a ResourceRecordSet in the Zone to the changeset
// The supplied ResourceRecordSet must match one of the existing recordsets (obtained via List()) exactly.
Remove(ResourceRecordSet) ResourceRecordChangeset
// Apply applies the accumulated operations to the Zone.
Apply() error
}
type ResourceRecordSet interface {
// Name returns the name of the ResourceRecordSet, e.g. "www.example.com".
Name() string
// Rrdatas returns the Resource Record Datas of the record set.
Rrdatas() []string
// Ttl returns the time-to-live of the record set, in seconds.
Ttl() int64
// Type returns the type of the record set (A, CNAME, SRV, etc)
Type() rrstype.RrsType
}
/* ResourceRecordSetsEquivalent compares two ResourceRecordSets for semantic equivalence.
Go's equality operator doesn't work the way we want it to in this case,
hence the need for this function.
More specifically (from the Go spec):
"Two struct values are equal if their corresponding non-blank fields are equal."
In our case, there may be some private internal member variables that may not be not equal,
but we want the two structs to be considered equivalent anyway, if the fields exposed
via their interfaces are equal.
*/
func ResourceRecordSetsEquivalent(r1, r2 ResourceRecordSet) bool {
if r1.Name() == r2.Name() && reflect.DeepEqual(r1.Rrdatas(), r2.Rrdatas()) && r1.Ttl() == r2.Ttl() && r1.Type() == r2.Type() {
return true
}
return false
}

View file

@ -0,0 +1,96 @@
/*
Copyright 2016 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 dnsprovider
import (
"testing"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
)
// Compile time interface check
var _ ResourceRecordSet = record{}
type record struct {
name string
rrdatas []string
ttl int64
type_ string
}
func (r record) Name() string {
return r.name
}
func (r record) Ttl() int64 {
return r.ttl
}
func (r record) Rrdatas() []string {
return r.rrdatas
}
func (r record) Type() rrstype.RrsType {
return rrstype.RrsType(r.type_)
}
const testDNSZone string = "foo.com"
var testData = []struct {
inputs [2]record
expectedOutput bool
}{
{
[2]record{
{"foo", []string{"1.2.3.4", "5,6,7,8"}, 180, "A"}, // Identical
{"foo", []string{"1.2.3.4", "5,6,7,8"}, 180, "A"}}, true,
},
{
[2]record{
{"foo", []string{"1.2.3.4", "5,6,7,8"}, 180, "A"}, // Identical except Name
{"bar", []string{"1.2.3.4", "5,6,7,8"}, 180, "A"}}, false,
},
{
[2]record{
{"foo", []string{"1.2.3.4", "5,6,7,8"}, 180, "A"}, // Identical except Rrdata
{"foo", []string{"1.2.3.4", "5,6,7,9"}, 180, "A"}}, false,
},
{
[2]record{
{"foo", []string{"1.2.3.4", "5,6,7,8"}, 180, "A"}, // Identical except Rrdata ordering reversed
{"foo", []string{"5,6,7,8", "1.2.3.4"}, 180, "A"}}, false,
},
{
[2]record{
{"foo", []string{"1.2.3.4", "5,6,7,8"}, 180, "A"}, // Identical except TTL
{"foo", []string{"1.2.3.4", "5,6,7,8"}, 150, "A"}}, false,
},
{
[2]record{
{"foo", []string{"1.2.3.4", "5,6,7,8"}, 180, "A"}, // Identical except Type
{"foo", []string{"1.2.3.4", "5,6,7,8"}, 180, "CNAME"}}, false,
},
}
func TestEquivalent(t *testing.T) {
for _, test := range testData {
output := ResourceRecordSetsEquivalent(test.inputs[0], test.inputs[1])
if output != test.expectedOutput {
t.Errorf("Expected equivalence comparison of %q and %q to yield %v, but it vielded %v", test.inputs[0], test.inputs[1], test.expectedOutput, output)
}
}
}

View file

@ -0,0 +1,21 @@
/*
Copyright 2016 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.
*/
/*
dnsprovider supplies interfaces for dns service providers (e.g. Google Cloud DNS, AWS route53, etc).
Implementations exist in the providers sub-package
*/
package dnsprovider // import "k8s.io/kubernetes/federation/pkg/dnsprovider"

View file

@ -0,0 +1,109 @@
/*
Copyright 2016 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 dnsprovider
import (
"fmt"
"io"
"os"
"sync"
"github.com/golang/glog"
)
// Factory is a function that returns a dnsprovider.Interface.
// The config parameter provides an io.Reader handler to the factory in
// order to load specific configurations. If no configuration is provided
// the parameter is nil.
type Factory func(config io.Reader) (Interface, error)
// All registered dns providers.
var providersMutex sync.Mutex
var providers = make(map[string]Factory)
// RegisterDnsProvider registers a dnsprovider.Factory by name. This
// is expected to happen during startup.
func RegisterDnsProvider(name string, cloud Factory) {
providersMutex.Lock()
defer providersMutex.Unlock()
if _, found := providers[name]; found {
glog.Fatalf("DNS provider %q was registered twice", name)
}
glog.V(1).Infof("Registered DNS provider %q", name)
providers[name] = cloud
}
// GetDnsProvider creates an instance of the named DNS provider, or nil if
// the name is not known. The error return is only used if the named provider
// was known but failed to initialize. The config parameter specifies the
// io.Reader handler of the configuration file for the DNS provider, or nil
// for no configuation.
func GetDnsProvider(name string, config io.Reader) (Interface, error) {
providersMutex.Lock()
defer providersMutex.Unlock()
f, found := providers[name]
if !found {
return nil, nil
}
return f(config)
}
// Returns a list of registered dns providers.
func RegisteredDnsProviders() []string {
registeredProviders := make([]string, len(providers))
i := 0
for provider := range providers {
registeredProviders[i] = provider
i = i + 1
}
return registeredProviders
}
// InitDnsProvider creates an instance of the named DNS provider.
func InitDnsProvider(name string, configFilePath string) (Interface, error) {
var dns Interface
var err error
if name == "" {
glog.Info("No DNS provider specified.")
return nil, nil
}
if configFilePath != "" {
var config *os.File
config, err = os.Open(configFilePath)
if err != nil {
return nil, fmt.Errorf("Couldn't open DNS provider configuration %s: %#v", configFilePath, err)
}
defer config.Close()
dns, err = GetDnsProvider(name, config)
} else {
// Pass explicit nil so plugins can actually check for nil. See
// "Why is my nil error value not equal to nil?" in golang.org/doc/faq.
dns, err = GetDnsProvider(name, nil)
}
if err != nil {
return nil, fmt.Errorf("could not init DNS provider %q: %v", name, err)
}
if dns == nil {
return nil, fmt.Errorf("unknown DNS provider %q", name)
}
return dns, nil
}

View file

@ -0,0 +1,49 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = [
"interface.go",
"route53.go",
"rrchangeset.go",
"rrset.go",
"rrsets.go",
"zone.go",
"zones.go",
],
tags = ["automanaged"],
deps = [
"//federation/pkg/dnsprovider:go_default_library",
"//federation/pkg/dnsprovider/providers/aws/route53/stubs:go_default_library",
"//federation/pkg/dnsprovider/rrstype:go_default_library",
"//pkg/util/uuid:go_default_library",
"//vendor:github.com/aws/aws-sdk-go/aws",
"//vendor:github.com/aws/aws-sdk-go/aws/session",
"//vendor:github.com/aws/aws-sdk-go/service/route53",
],
)
go_test(
name = "go_default_test",
srcs = ["route53_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/pkg/dnsprovider:go_default_library",
"//federation/pkg/dnsprovider/providers/aws/route53/stubs:go_default_library",
"//federation/pkg/dnsprovider/rrstype:go_default_library",
"//federation/pkg/dnsprovider/tests:go_default_library",
"//vendor:github.com/aws/aws-sdk-go/aws",
"//vendor:github.com/aws/aws-sdk-go/service/route53",
],
)

View file

@ -0,0 +1,39 @@
/*
Copyright 2016 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 route53
import (
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/aws/route53/stubs"
)
// Compile time check for interface adeherence
var _ dnsprovider.Interface = Interface{}
type Interface struct {
service stubs.Route53API
}
// newInterfaceWithStub facilitates stubbing out the underlying AWS Route53
// library for testing purposes. It returns an provider-independent interface.
func newInterfaceWithStub(service stubs.Route53API) *Interface {
return &Interface{service}
}
func (i Interface) Zones() (zones dnsprovider.Zones, supported bool) {
return Zones{&i}, true
}

View file

@ -0,0 +1,44 @@
/*
Copyright 2016 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.
*/
// route53 is the implementation of pkg/dnsprovider interface for AWS Route53
package route53
import (
"io"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/route53"
)
const (
ProviderName = "aws-route53"
)
func init() {
dnsprovider.RegisterDnsProvider(ProviderName, func(config io.Reader) (dnsprovider.Interface, error) {
return newRoute53(config)
})
}
// newRoute53 creates a new instance of an AWS Route53 DNS Interface.
func newRoute53(config io.Reader) (*Interface, error) {
// Connect to AWS Route53 - TODO: Do more sophisticated auth
svc := route53.New(session.New())
return newInterfaceWithStub(svc), nil
}

View file

@ -0,0 +1,295 @@
/*
Copyright 2016 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 route53
import (
"flag"
"fmt"
"os"
"testing"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
route53testing "k8s.io/kubernetes/federation/pkg/dnsprovider/providers/aws/route53/stubs"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/route53"
"k8s.io/kubernetes/federation/pkg/dnsprovider/tests"
)
func newTestInterface() (dnsprovider.Interface, error) {
// Use this to test the real cloud service.
// return dnsprovider.GetDnsProvider(ProviderName, strings.NewReader("\n[global]\nproject-id = federation0-cluster00"))
return newFakeInterface() // Use this to stub out the entire cloud service
}
func newFakeInterface() (dnsprovider.Interface, error) {
var service route53testing.Route53API
service = route53testing.NewRoute53APIStub()
iface := newInterfaceWithStub(service)
// Add a fake zone to test against.
params := &route53.CreateHostedZoneInput{
CallerReference: aws.String("Nonce"), // Required
Name: aws.String("example.com"), // Required
}
_, err := iface.service.CreateHostedZone(params)
if err != nil {
return nil, err
}
return iface, nil
}
var interface_ dnsprovider.Interface
func TestMain(m *testing.M) {
fmt.Printf("Parsing flags.\n")
flag.Parse()
var err error
fmt.Printf("Getting new test interface.\n")
interface_, err = newTestInterface()
if err != nil {
fmt.Printf("Error creating interface: %v", err)
os.Exit(1)
}
fmt.Printf("Running tests...\n")
os.Exit(m.Run())
}
// zones returns the zones interface for the configured dns provider account/project,
// or fails if it can't be found
func zones(t *testing.T) dnsprovider.Zones {
zonesInterface, supported := interface_.Zones()
if !supported {
t.Fatalf("Zones interface not supported by interface %v", interface_)
} else {
t.Logf("Got zones %v\n", zonesInterface)
}
return zonesInterface
}
// firstZone returns the first zone for the configured dns provider account/project,
// or fails if it can't be found
func firstZone(t *testing.T) dnsprovider.Zone {
t.Logf("Getting zones")
z := zones(t)
zones, err := z.List()
if err != nil {
t.Fatalf("Failed to list zones: %v", err)
} else {
t.Logf("Got zone list: %v\n", zones)
}
if len(zones) < 1 {
t.Fatalf("Zone listing returned %d, expected >= %d", len(zones), 1)
} else {
t.Logf("Got at least 1 zone in list:%v\n", zones[0])
}
return zones[0]
}
/* rrs returns the ResourceRecordSets interface for a given zone */
func rrs(t *testing.T, zone dnsprovider.Zone) (r dnsprovider.ResourceRecordSets) {
rrsets, supported := zone.ResourceRecordSets()
if !supported {
t.Fatalf("ResourceRecordSets interface not supported by zone %v", zone)
return r
}
return rrsets
}
func listRrsOrFail(t *testing.T, rrsets dnsprovider.ResourceRecordSets) []dnsprovider.ResourceRecordSet {
rrset, err := rrsets.List()
if err != nil {
t.Fatalf("Failed to list recordsets: %v", err)
} else {
if len(rrset) < 0 {
t.Fatalf("Record set length=%d, expected >=0", len(rrset))
} else {
t.Logf("Got %d recordsets: %v", len(rrset), rrset)
}
}
return rrset
}
func getExampleRrs(zone dnsprovider.Zone) dnsprovider.ResourceRecordSet {
rrsets, _ := zone.ResourceRecordSets()
return rrsets.New("www11."+zone.Name(), []string{"10.10.10.10", "169.20.20.20"}, 180, rrstype.A)
}
func getInvalidRrs(zone dnsprovider.Zone) dnsprovider.ResourceRecordSet {
rrsets, _ := zone.ResourceRecordSets()
return rrsets.New("www12."+zone.Name(), []string{"rubbish", "rubbish"}, 180, rrstype.A)
}
func addRrsetOrFail(t *testing.T, rrsets dnsprovider.ResourceRecordSets, rrset dnsprovider.ResourceRecordSet) {
err := rrsets.StartChangeset().Add(rrset).Apply()
if err != nil {
t.Fatalf("Failed to add recordsets: %v", err)
}
}
/* TestZonesList verifies that listing of zones succeeds */
func TestZonesList(t *testing.T) {
firstZone(t)
}
/* TestZonesID verifies that the id of the zone is returned with the prefix removed */
func TestZonesID(t *testing.T) {
zone := firstZone(t)
// Check /hostedzone/ prefix is removed
zoneID := zone.ID()
if zoneID != zone.Name() {
t.Fatalf("Unexpected zone id: %q", zoneID)
}
}
/* TestZoneAddSuccess verifies that addition of a valid managed DNS zone succeeds */
func TestZoneAddSuccess(t *testing.T) {
testZoneName := "ubernetes.testing"
z := zones(t)
input, err := z.New(testZoneName)
if err != nil {
t.Errorf("Failed to allocate new zone object %s: %v", testZoneName, err)
}
zone, err := z.Add(input)
if err != nil {
t.Errorf("Failed to create new managed DNS zone %s: %v", testZoneName, err)
}
defer func(zone dnsprovider.Zone) {
if zone != nil {
if err := z.Remove(zone); err != nil {
t.Errorf("Failed to delete zone %v: %v", zone, err)
}
}
}(zone)
t.Logf("Successfully added managed DNS zone: %v", zone)
}
/* TestResourceRecordSetsList verifies that listing of RRS's succeeds */
func TestResourceRecordSetsList(t *testing.T) {
listRrsOrFail(t, rrs(t, firstZone(t)))
}
/* TestResourceRecordSetsAddSuccess verifies that addition of a valid RRS succeeds */
func TestResourceRecordSetsAddSuccess(t *testing.T) {
zone := firstZone(t)
sets := rrs(t, zone)
set := getExampleRrs(zone)
addRrsetOrFail(t, sets, set)
defer sets.StartChangeset().Remove(set).Apply()
t.Logf("Successfully added resource record set: %v", set)
}
/* TestResourceRecordSetsAdditionVisible verifies that added RRS is visible after addition */
func TestResourceRecordSetsAdditionVisible(t *testing.T) {
zone := firstZone(t)
sets := rrs(t, zone)
rrset := getExampleRrs(zone)
addRrsetOrFail(t, sets, rrset)
defer sets.StartChangeset().Remove(rrset).Apply()
t.Logf("Successfully added resource record set: %v", rrset)
found := false
for _, record := range listRrsOrFail(t, sets) {
if record.Name() == rrset.Name() {
found = true
break
}
}
if !found {
t.Errorf("Failed to find added resource record set %s", rrset.Name())
}
}
/* TestResourceRecordSetsAddDuplicateFail verifies that addition of a duplicate RRS fails */
func TestResourceRecordSetsAddDuplicateFail(t *testing.T) {
zone := firstZone(t)
sets := rrs(t, zone)
rrset := getExampleRrs(zone)
addRrsetOrFail(t, sets, rrset)
defer sets.StartChangeset().Remove(rrset).Apply()
t.Logf("Successfully added resource record set: %v", rrset)
// Try to add it again, and verify that the call fails.
err := sets.StartChangeset().Add(rrset).Apply()
if err == nil {
defer sets.StartChangeset().Remove(rrset).Apply()
t.Errorf("Should have failed to add duplicate resource record %v, but succeeded instead.", rrset)
} else {
t.Logf("Correctly failed to add duplicate resource record %v: %v", rrset, err)
}
}
/* TestResourceRecordSetsRemove verifies that the removal of an existing RRS succeeds */
func TestResourceRecordSetsRemove(t *testing.T) {
zone := firstZone(t)
sets := rrs(t, zone)
rrset := getExampleRrs(zone)
addRrsetOrFail(t, sets, rrset)
err := sets.StartChangeset().Remove(rrset).Apply()
if err != nil {
// Try again to clean up.
defer sets.StartChangeset().Remove(rrset).Apply()
t.Errorf("Failed to remove resource record set %v after adding", rrset)
} else {
t.Logf("Successfully removed resource set %v after adding", rrset)
}
}
/* TestResourceRecordSetsRemoveGone verifies that a removed RRS no longer exists */
func TestResourceRecordSetsRemoveGone(t *testing.T) {
zone := firstZone(t)
sets := rrs(t, zone)
rrset := getExampleRrs(zone)
addRrsetOrFail(t, sets, rrset)
err := sets.StartChangeset().Remove(rrset).Apply()
if err != nil {
// Try again to clean up.
defer sets.StartChangeset().Remove(rrset).Apply()
t.Errorf("Failed to remove resource record set %v after adding", rrset)
} else {
t.Logf("Successfully removed resource set %v after adding", rrset)
}
// Check that it's gone
list := listRrsOrFail(t, sets)
found := false
for _, set := range list {
if set.Name() == rrset.Name() {
found = true
break
}
}
if found {
t.Errorf("Deleted resource record set %v is still present", rrset)
}
}
/* TestResourceRecordSetsReplace verifies that replacing an RRS works */
func TestResourceRecordSetsReplace(t *testing.T) {
zone := firstZone(t)
tests.CommonTestResourceRecordSetsReplace(t, zone)
}
/* TestResourceRecordSetsReplaceAll verifies that we can remove an RRS and create one with a different name*/
func TestResourceRecordSetsReplaceAll(t *testing.T) {
zone := firstZone(t)
tests.CommonTestResourceRecordSetsReplaceAll(t, zone)
}
/* TestResourceRecordSetsHonorsType verifies that we can add records of the same name but different types */
func TestResourceRecordSetsDifferentTypes(t *testing.T) {
zone := firstZone(t)
tests.CommonTestResourceRecordSetsDifferentTypes(t, zone)
}

View file

@ -0,0 +1,101 @@
/*
Copyright 2016 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 route53
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/route53"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
)
// Compile time check for interface adherence
var _ dnsprovider.ResourceRecordChangeset = &ResourceRecordChangeset{}
type ResourceRecordChangeset struct {
zone *Zone
rrsets *ResourceRecordSets
additions []dnsprovider.ResourceRecordSet
removals []dnsprovider.ResourceRecordSet
}
func (c *ResourceRecordChangeset) Add(rrset dnsprovider.ResourceRecordSet) dnsprovider.ResourceRecordChangeset {
c.additions = append(c.additions, rrset)
return c
}
func (c *ResourceRecordChangeset) Remove(rrset dnsprovider.ResourceRecordSet) dnsprovider.ResourceRecordChangeset {
c.removals = append(c.removals, rrset)
return c
}
// buildChange converts a dnsprovider.ResourceRecordSet to a route53.Change request
func buildChange(action string, rrs dnsprovider.ResourceRecordSet) *route53.Change {
change := &route53.Change{
Action: aws.String(action),
ResourceRecordSet: &route53.ResourceRecordSet{
Name: aws.String(rrs.Name()),
Type: aws.String(string(rrs.Type())),
TTL: aws.Int64(rrs.Ttl()),
},
}
for _, rrdata := range rrs.Rrdatas() {
rr := &route53.ResourceRecord{
Value: aws.String(rrdata),
}
change.ResourceRecordSet.ResourceRecords = append(change.ResourceRecordSet.ResourceRecords, rr)
}
return change
}
func (c *ResourceRecordChangeset) Apply() error {
hostedZoneID := c.zone.impl.Id
var changes []*route53.Change
for _, removal := range c.removals {
change := buildChange(route53.ChangeActionDelete, removal)
changes = append(changes, change)
}
for _, addition := range c.additions {
change := buildChange(route53.ChangeActionCreate, addition)
changes = append(changes, change)
}
if len(changes) == 0 {
return nil
}
service := c.zone.zones.interface_.service
request := &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &route53.ChangeBatch{
Changes: changes,
},
HostedZoneId: hostedZoneID,
}
_, err := service.ChangeResourceRecordSets(request)
if err != nil {
// Cast err to awserr.Error to get the Code and
// Message from an error.
return err
}
return nil
}

View file

@ -0,0 +1,53 @@
/*
Copyright 2016 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 route53
import (
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
"github.com/aws/aws-sdk-go/service/route53"
)
// Compile time check for interface adherence
var _ dnsprovider.ResourceRecordSet = ResourceRecordSet{}
type ResourceRecordSet struct {
impl *route53.ResourceRecordSet
rrsets *ResourceRecordSets
}
func (rrset ResourceRecordSet) Name() string {
return *rrset.impl.Name
}
func (rrset ResourceRecordSet) Rrdatas() []string {
// Sigh - need to unpack the strings out of the route53 ResourceRecords
result := make([]string, len(rrset.impl.ResourceRecords))
for i, record := range rrset.impl.ResourceRecords {
result[i] = *record.Value
}
return result
}
func (rrset ResourceRecordSet) Ttl() int64 {
return *rrset.impl.TTL
}
func (rrset ResourceRecordSet) Type() rrstype.RrsType {
return rrstype.RrsType(*rrset.impl.Type)
}

View file

@ -0,0 +1,75 @@
/*
Copyright 2016 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 route53
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/route53"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
)
// Compile time check for interface adherence
var _ dnsprovider.ResourceRecordSets = ResourceRecordSets{}
type ResourceRecordSets struct {
zone *Zone
}
func (rrsets ResourceRecordSets) List() ([]dnsprovider.ResourceRecordSet, error) {
input := route53.ListResourceRecordSetsInput{
HostedZoneId: rrsets.zone.impl.Id,
}
var list []dnsprovider.ResourceRecordSet
err := rrsets.zone.zones.interface_.service.ListResourceRecordSetsPages(&input, func(page *route53.ListResourceRecordSetsOutput, lastPage bool) bool {
for _, rrset := range page.ResourceRecordSets {
list = append(list, &ResourceRecordSet{rrset, &rrsets})
}
return true
})
if err != nil {
return nil, err
}
return list, nil
}
func (r ResourceRecordSets) StartChangeset() dnsprovider.ResourceRecordChangeset {
return &ResourceRecordChangeset{
zone: r.zone,
rrsets: &r,
}
}
func (r ResourceRecordSets) New(name string, rrdatas []string, ttl int64, rrstype rrstype.RrsType) dnsprovider.ResourceRecordSet {
rrstypeStr := string(rrstype)
rrs := &route53.ResourceRecordSet{
Name: &name,
Type: &rrstypeStr,
TTL: &ttl,
}
for _, rrdata := range rrdatas {
rrs.ResourceRecords = append(rrs.ResourceRecords, &route53.ResourceRecord{
Value: aws.String(rrdata),
})
}
return ResourceRecordSet{
rrs,
&r,
}
}

View file

@ -0,0 +1,21 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["route53api.go"],
tags = ["automanaged"],
deps = [
"//vendor:github.com/aws/aws-sdk-go/aws",
"//vendor:github.com/aws/aws-sdk-go/service/route53",
],
)

View file

@ -0,0 +1,133 @@
/*
Copyright 2016 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.
*/
/* internal implements a stub for the AWS Route53 API, used primarily for unit testing purposes */
package stubs
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/route53"
)
// Compile time check for interface conformance
var _ Route53API = &Route53APIStub{}
/* Route53API is the subset of the AWS Route53 API that we actually use. Add methods as required. Signatures must match exactly. */
type Route53API interface {
ListResourceRecordSetsPages(input *route53.ListResourceRecordSetsInput, fn func(p *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool)) error
ChangeResourceRecordSets(*route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error)
ListHostedZonesPages(input *route53.ListHostedZonesInput, fn func(p *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool)) error
CreateHostedZone(*route53.CreateHostedZoneInput) (*route53.CreateHostedZoneOutput, error)
DeleteHostedZone(*route53.DeleteHostedZoneInput) (*route53.DeleteHostedZoneOutput, error)
}
// Route53APIStub is a minimal implementation of Route53API, used primarily for unit testing.
// See http://http://docs.aws.amazon.com/sdk-for-go/api/service/route53.html for descriptions
// of all of its methods.
type Route53APIStub struct {
zones map[string]*route53.HostedZone
recordSets map[string]map[string][]*route53.ResourceRecordSet
}
// NewRoute53APIStub returns an initlialized Route53APIStub
func NewRoute53APIStub() *Route53APIStub {
return &Route53APIStub{
zones: make(map[string]*route53.HostedZone),
recordSets: make(map[string]map[string][]*route53.ResourceRecordSet),
}
}
func (r *Route53APIStub) ListResourceRecordSetsPages(input *route53.ListResourceRecordSetsInput, fn func(p *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool)) error {
output := route53.ListResourceRecordSetsOutput{} // TODO: Support optional input args.
if len(r.recordSets) <= 0 {
output.ResourceRecordSets = []*route53.ResourceRecordSet{}
} else if _, ok := r.recordSets[*input.HostedZoneId]; !ok {
output.ResourceRecordSets = []*route53.ResourceRecordSet{}
} else {
for _, rrsets := range r.recordSets[*input.HostedZoneId] {
for _, rrset := range rrsets {
output.ResourceRecordSets = append(output.ResourceRecordSets, rrset)
}
}
}
lastPage := true
fn(&output, lastPage)
return nil
}
func (r *Route53APIStub) ChangeResourceRecordSets(input *route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error) {
output := &route53.ChangeResourceRecordSetsOutput{}
recordSets, ok := r.recordSets[*input.HostedZoneId]
if !ok {
recordSets = make(map[string][]*route53.ResourceRecordSet)
}
for _, change := range input.ChangeBatch.Changes {
key := *change.ResourceRecordSet.Name + "::" + *change.ResourceRecordSet.Type
switch *change.Action {
case route53.ChangeActionCreate:
if _, found := recordSets[key]; found {
return nil, fmt.Errorf("Attempt to create duplicate rrset %s", key) // TODO: Return AWS errors with codes etc
}
recordSets[key] = append(recordSets[key], change.ResourceRecordSet)
case route53.ChangeActionDelete:
if _, found := recordSets[key]; !found {
return nil, fmt.Errorf("Attempt to delete non-existent rrset %s", key) // TODO: Check other fields too
}
delete(recordSets, key)
case route53.ChangeActionUpsert:
// TODO - not used yet
}
}
r.recordSets[*input.HostedZoneId] = recordSets
return output, nil // TODO: We should ideally return status etc, but we don't' use that yet.
}
func (r *Route53APIStub) ListHostedZonesPages(input *route53.ListHostedZonesInput, fn func(p *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool)) error {
output := &route53.ListHostedZonesOutput{}
for _, zone := range r.zones {
output.HostedZones = append(output.HostedZones, zone)
}
lastPage := true
fn(output, lastPage)
return nil
}
func (r *Route53APIStub) CreateHostedZone(input *route53.CreateHostedZoneInput) (*route53.CreateHostedZoneOutput, error) {
name := aws.StringValue(input.Name)
id := "/hostedzone/" + name
if _, ok := r.zones[id]; ok {
return nil, fmt.Errorf("Error creating hosted DNS zone: %s already exists", id)
}
r.zones[id] = &route53.HostedZone{
Id: aws.String(id),
Name: aws.String(name),
}
return &route53.CreateHostedZoneOutput{HostedZone: r.zones[id]}, nil
}
func (r *Route53APIStub) DeleteHostedZone(input *route53.DeleteHostedZoneInput) (*route53.DeleteHostedZoneOutput, error) {
if _, ok := r.zones[*input.Id]; !ok {
return nil, fmt.Errorf("Error deleting hosted DNS zone: %s does not exist", *input.Id)
}
if len(r.recordSets[*input.Id]) > 0 {
return nil, fmt.Errorf("Error deleting hosted DNS zone: %s has resource records", *input.Id)
}
delete(r.zones, *input.Id)
return &route53.DeleteHostedZoneOutput{}, nil
}

View file

@ -0,0 +1,47 @@
/*
Copyright 2016 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 route53
import (
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/route53"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
)
// Compile time check for interface adeherence
var _ dnsprovider.Zone = &Zone{}
type Zone struct {
impl *route53.HostedZone
zones *Zones
}
func (zone *Zone) Name() string {
return aws.StringValue(zone.impl.Name)
}
func (zone *Zone) ID() string {
id := aws.StringValue(zone.impl.Id)
id = strings.TrimPrefix(id, "/hostedzone/")
return id
}
func (zone *Zone) ResourceRecordSets() (dnsprovider.ResourceRecordSets, bool) {
return &ResourceRecordSets{zone}, true
}

View file

@ -0,0 +1,73 @@
/*
Copyright 2016 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 route53
import (
"github.com/aws/aws-sdk-go/service/route53"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/pkg/util/uuid"
)
// Compile time check for interface adeherence
var _ dnsprovider.Zones = Zones{}
type Zones struct {
interface_ *Interface
}
func (zones Zones) List() ([]dnsprovider.Zone, error) {
var zoneList []dnsprovider.Zone
input := route53.ListHostedZonesInput{}
err := zones.interface_.service.ListHostedZonesPages(&input, func(page *route53.ListHostedZonesOutput, lastPage bool) bool {
for _, zone := range page.HostedZones {
zoneList = append(zoneList, &Zone{zone, &zones})
}
return true
})
if err != nil {
return []dnsprovider.Zone{}, err
}
return zoneList, nil
}
func (zones Zones) Add(zone dnsprovider.Zone) (dnsprovider.Zone, error) {
dnsName := zone.Name()
callerReference := string(uuid.NewUUID())
input := route53.CreateHostedZoneInput{Name: &dnsName, CallerReference: &callerReference}
output, err := zones.interface_.service.CreateHostedZone(&input)
if err != nil {
return nil, err
}
return &Zone{output.HostedZone, &zones}, nil
}
func (zones Zones) Remove(zone dnsprovider.Zone) error {
zoneId := zone.(*Zone).impl.Id
input := route53.DeleteHostedZoneInput{Id: zoneId}
_, err := zones.interface_.service.DeleteHostedZone(&input)
if err != nil {
return err
}
return nil
}
func (zones Zones) New(name string) (dnsprovider.Zone, error) {
id := string(uuid.NewUUID())
managedZone := route53.HostedZone{Id: &id, Name: &name}
return &Zone{&managedZone, &zones}, nil
}

View file

@ -0,0 +1,52 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = [
"clouddns.go",
"interface.go",
"rrchangeset.go",
"rrset.go",
"rrsets.go",
"zone.go",
"zones.go",
],
tags = ["automanaged"],
deps = [
"//federation/pkg/dnsprovider:go_default_library",
"//federation/pkg/dnsprovider/providers/google/clouddns/internal:go_default_library",
"//federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces:go_default_library",
"//federation/pkg/dnsprovider/providers/google/clouddns/internal/stubs:go_default_library",
"//federation/pkg/dnsprovider/rrstype:go_default_library",
"//pkg/cloudprovider/providers/gce:go_default_library",
"//vendor:cloud.google.com/go/compute/metadata",
"//vendor:github.com/golang/glog",
"//vendor:golang.org/x/oauth2",
"//vendor:golang.org/x/oauth2/google",
"//vendor:google.golang.org/api/compute/v1",
"//vendor:google.golang.org/api/dns/v1",
"//vendor:gopkg.in/gcfg.v1",
],
)
go_test(
name = "go_default_test",
srcs = ["clouddns_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/pkg/dnsprovider:go_default_library",
"//federation/pkg/dnsprovider/rrstype:go_default_library",
"//federation/pkg/dnsprovider/tests:go_default_library",
],
)

View file

@ -0,0 +1,116 @@
/*
Copyright 2016 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.
*/
// clouddns is the implementation of pkg/dnsprovider interface for Google Cloud DNS
package clouddns
import (
"io"
"cloud.google.com/go/compute/metadata"
"github.com/golang/glog"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
compute "google.golang.org/api/compute/v1"
dns "google.golang.org/api/dns/v1"
gcfg "gopkg.in/gcfg.v1"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/stubs"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce"
)
const (
ProviderName = "google-clouddns"
)
func init() {
dnsprovider.RegisterDnsProvider(ProviderName, func(config io.Reader) (dnsprovider.Interface, error) {
return newCloudDns(config)
})
}
type Config struct {
Global struct {
TokenURL string `gcfg:"token-url"`
TokenBody string `gcfg:"token-body"`
ProjectID string `gcfg:"project-id"`
}
}
// newCloudDns creates a new instance of a Google Cloud DNS Interface.
func newCloudDns(config io.Reader) (*Interface, error) {
projectID, _ := metadata.ProjectID() // On error we get an empty string, which is fine for now.
var tokenSource oauth2.TokenSource
// Possibly override defaults with config below
if config != nil {
var cfg Config
if err := gcfg.ReadInto(&cfg, config); err != nil {
glog.Errorf("Couldn't read config: %v", err)
return nil, err
}
glog.Infof("Using Google Cloud DNS provider config %+v", cfg)
if cfg.Global.ProjectID != "" {
projectID = cfg.Global.ProjectID
}
if cfg.Global.TokenURL != "" {
tokenSource = gce.NewAltTokenSource(cfg.Global.TokenURL, cfg.Global.TokenBody)
}
}
return CreateInterface(projectID, tokenSource)
}
// CreateInterface creates a clouddns.Interface object using the specified parameters.
// If no tokenSource is specified, uses oauth2.DefaultTokenSource.
func CreateInterface(projectID string, tokenSource oauth2.TokenSource) (*Interface, error) {
if tokenSource == nil {
var err error
tokenSource, err = google.DefaultTokenSource(
oauth2.NoContext,
compute.CloudPlatformScope,
compute.ComputeScope)
glog.Infof("Using DefaultTokenSource %#v", tokenSource)
if err != nil {
return nil, err
}
} else {
glog.Infof("Using existing Token Source %#v", tokenSource)
}
oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource)
service, err := dns.New(oauthClient)
if err != nil {
glog.Errorf("Failed to get Cloud DNS client: %v", err)
}
glog.Infof("Successfully got DNS service: %v\n", service)
return newInterfaceWithStub(projectID, internal.NewService(service)), nil
}
// NewFakeInterface returns a fake clouddns interface, useful for unit testing purposes.
func NewFakeInterface() (dnsprovider.Interface, error) {
service := stubs.NewService()
interface_ := newInterfaceWithStub("", service)
zones := service.ManagedZones_
// Add a fake zone to test against.
zone := &stubs.ManagedZone{Service: zones, Name_: "example.com", Rrsets: []stubs.ResourceRecordSet{}, Id_: 1}
call := zones.Create(interface_.project(), zone)
if _, err := call.Do(); err != nil {
return nil, err
}
return interface_, nil
}

View file

@ -0,0 +1,273 @@
/*
Copyright 2016 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 clouddns
import (
"flag"
"fmt"
"os"
"testing"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
"k8s.io/kubernetes/federation/pkg/dnsprovider/tests"
)
func newTestInterface() (dnsprovider.Interface, error) {
// Use this to test the real cloud service - insert appropriate project-id. Default token source will be used. See
// https://github.com/golang/oauth2/blob/master/google/default.go for details.
// return dnsprovider.GetDnsProvider(ProviderName, strings.NewReader("\n[global]\nproject-id = federation0-cluster00"))
return NewFakeInterface() // Use this to stub out the entire cloud service
}
var interface_ dnsprovider.Interface
func TestMain(m *testing.M) {
flag.Parse()
var err error
interface_, err = newTestInterface()
if err != nil {
fmt.Printf("Error creating interface: %v", err)
os.Exit(1)
}
os.Exit(m.Run())
}
// zones returns the zones interface for the configured dns provider account/project,
// or fails if it can't be found
func zones(t *testing.T) dnsprovider.Zones {
zonesInterface, supported := interface_.Zones()
if !supported {
t.Fatalf("Zones interface not supported by interface %v", interface_)
} else {
t.Logf("Got zones %v\n", zonesInterface)
}
return zonesInterface
}
// firstZone returns the first zone for the configured dns provider account/project,
// or fails if it can't be found
func firstZone(t *testing.T) dnsprovider.Zone {
t.Logf("Getting zones")
zones, err := zones(t).List()
if err != nil {
t.Fatalf("Failed to list zones: %v", err)
} else {
t.Logf("Got zone list: %v\n", zones)
}
if len(zones) < 1 {
t.Fatalf("Zone listing returned %d, expected >= %d", len(zones), 1)
} else {
t.Logf("Got at least 1 zone in list:%v\n", zones[0])
}
return zones[0]
}
/* rrs returns the ResourceRecordSets interface for a given zone */
func rrs(t *testing.T, zone dnsprovider.Zone) (r dnsprovider.ResourceRecordSets) {
rrsets, supported := zone.ResourceRecordSets()
if !supported {
t.Fatalf("ResourceRecordSets interface not supported by zone %v", zone)
return r
}
return rrsets
}
func listRrsOrFail(t *testing.T, rrsets dnsprovider.ResourceRecordSets) []dnsprovider.ResourceRecordSet {
rrset, err := rrsets.List()
if err != nil {
t.Fatalf("Failed to list recordsets: %v", err)
} else {
if len(rrset) < 0 {
t.Fatalf("Record set length=%d, expected >=0", len(rrset))
} else {
t.Logf("Got %d recordsets: %v", len(rrset), rrset)
}
}
return rrset
}
func getExampleRrs(zone dnsprovider.Zone) dnsprovider.ResourceRecordSet {
rrsets, _ := zone.ResourceRecordSets()
return rrsets.New("www11."+zone.Name(), []string{"10.10.10.10", "169.20.20.20"}, 180, rrstype.A)
}
func getInvalidRrs(zone dnsprovider.Zone) dnsprovider.ResourceRecordSet {
rrsets, _ := zone.ResourceRecordSets()
return rrsets.New("www12."+zone.Name(), []string{"rubbish", "rubbish"}, 180, rrstype.A)
}
func addRrsetOrFail(t *testing.T, rrsets dnsprovider.ResourceRecordSets, rrset dnsprovider.ResourceRecordSet) {
err := rrsets.StartChangeset().Add(rrset).Apply()
if err != nil {
t.Fatalf("Failed to add recordsets: %v", err)
}
}
/* TestZonesList verifies that listing of zones succeeds */
func TestZonesList(t *testing.T) {
firstZone(t)
}
/* TestZonesID verifies that the id of the zone is returned with the prefix removed */
func TestZonesID(t *testing.T) {
zone := firstZone(t)
zoneID := zone.ID()
if zoneID != "1" {
t.Fatalf("Unexpected zone id: %q", zoneID)
}
}
/* TestZoneAddSuccess verifies that addition of a valid managed DNS zone succeeds */
func TestZoneAddSuccess(t *testing.T) {
testZoneName := "ubernetesv2.test."
t.Logf("Getting zones")
z := zones(t)
t.Logf("Got zones, making new Zone")
input, err := z.New(testZoneName)
if err != nil {
t.Errorf("Failed to allocate new zone object %s: %v", testZoneName, err)
}
zone, err := z.Add(input)
if err != nil {
t.Errorf("Failed to create new managed DNS zone %s: %v", testZoneName, err)
}
defer func(zone dnsprovider.Zone) {
if zone != nil {
if err := z.Remove(zone); err != nil {
t.Errorf("Failed to delete zone %v: %v", zone, err)
}
}
}(zone)
t.Logf("Successfully added managed DNS zone: %v", zone)
}
/* TestResourceRecordSetsList verifies that listing of RRS's succeeds */
func TestResourceRecordSetsList(t *testing.T) {
listRrsOrFail(t, rrs(t, firstZone(t)))
}
/* TestResourceRecordSetsAddSuccess verifies that addition of a valid RRS succeeds */
func TestResourceRecordSetsAddSuccess(t *testing.T) {
zone := firstZone(t)
sets := rrs(t, zone)
set := getExampleRrs(zone)
addRrsetOrFail(t, sets, set)
defer sets.StartChangeset().Remove(set).Apply()
t.Logf("Successfully added resource record set: %v", set)
}
/* TestResourceRecordSetsAdditionVisible verifies that added RRS is visible after addition */
func TestResourceRecordSetsAdditionVisible(t *testing.T) {
zone := firstZone(t)
sets := rrs(t, zone)
rrset := getExampleRrs(zone)
addRrsetOrFail(t, sets, rrset)
defer sets.StartChangeset().Remove(rrset).Apply()
t.Logf("Successfully added resource record set: %v", rrset)
found := false
for _, record := range listRrsOrFail(t, sets) {
if record.Name() == rrset.Name() {
found = true
break
}
}
if !found {
t.Errorf("Failed to find added resource record set %s", rrset.Name())
}
}
/* TestResourceRecordSetsAddDuplicateFail verifies that addition of a duplicate RRS fails */
func TestResourceRecordSetsAddDuplicateFail(t *testing.T) {
zone := firstZone(t)
sets := rrs(t, zone)
rrset := getExampleRrs(zone)
addRrsetOrFail(t, sets, rrset)
defer sets.StartChangeset().Remove(rrset).Apply()
t.Logf("Successfully added resource record set: %v", rrset)
// Try to add it again, and verify that the call fails.
err := sets.StartChangeset().Add(rrset).Apply()
if err == nil {
defer sets.StartChangeset().Remove(rrset).Apply()
t.Errorf("Should have failed to add duplicate resource record %v, but succeeded instead.", rrset)
} else {
t.Logf("Correctly failed to add duplicate resource record %v: %v", rrset, err)
}
}
/* TestResourceRecordSetsRemove verifies that the removal of an existing RRS succeeds */
func TestResourceRecordSetsRemove(t *testing.T) {
zone := firstZone(t)
sets := rrs(t, zone)
rrset := getExampleRrs(zone)
addRrsetOrFail(t, sets, rrset)
err := sets.StartChangeset().Remove(rrset).Apply()
if err != nil {
// Try again to clean up.
defer sets.StartChangeset().Remove(rrset).Apply()
t.Errorf("Failed to remove resource record set %v after adding: %v", rrset, err)
} else {
t.Logf("Successfully removed resource set %v after adding", rrset)
}
}
/* TestResourceRecordSetsRemoveGone verifies that a removed RRS no longer exists */
func TestResourceRecordSetsRemoveGone(t *testing.T) {
zone := firstZone(t)
sets := rrs(t, zone)
rrset := getExampleRrs(zone)
addRrsetOrFail(t, sets, rrset)
err := sets.StartChangeset().Remove(rrset).Apply()
if err != nil {
// Try again to clean up.
defer sets.StartChangeset().Remove(rrset).Apply()
t.Errorf("Failed to remove resource record set %v after adding: %v", rrset, err)
} else {
t.Logf("Successfully removed resource set %v after adding", rrset)
}
// Check that it's gone
list := listRrsOrFail(t, sets)
found := false
for _, set := range list {
if set.Name() == rrset.Name() {
found = true
break
}
}
if found {
t.Errorf("Deleted resource record set %v is still present", rrset)
}
}
/* TestResourceRecordSetsReplace verifies that replacing an RRS works */
func TestResourceRecordSetsReplace(t *testing.T) {
zone := firstZone(t)
tests.CommonTestResourceRecordSetsReplace(t, zone)
}
/* TestResourceRecordSetsReplaceAll verifies that we can remove an RRS and create one with a different name*/
func TestResourceRecordSetsReplaceAll(t *testing.T) {
zone := firstZone(t)
tests.CommonTestResourceRecordSetsReplaceAll(t, zone)
}
/* TestResourceRecordSetsHonorsType verifies that we can add records of the same name but different types */
func TestResourceRecordSetsDifferentTypes(t *testing.T) {
zone := firstZone(t)
tests.CommonTestResourceRecordSetsDifferentTypes(t, zone)
}

View file

@ -0,0 +1,43 @@
/*
Copyright 2016 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 clouddns
import (
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
var _ dnsprovider.Interface = Interface{}
type Interface struct {
project_ string
service interfaces.Service
}
// newInterfaceWithStub facilitates stubbing out the underlying Google Cloud DNS
// library for testing purposes. It returns an provider-independent interface.
func newInterfaceWithStub(project string, service interfaces.Service) *Interface {
return &Interface{project, service}
}
func (i Interface) Zones() (zones dnsprovider.Zones, supported bool) {
return Zones{i.service.ManagedZones(), &i}, true
}
func (i Interface) project() string {
return i.project_
}

View file

@ -0,0 +1,41 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = [
"change.go",
"changes_create_call.go",
"changes_service.go",
"clouddns.go",
"managed_zone.go",
"managed_zone_create_call.go",
"managed_zones_delete_call.go",
"managed_zones_get_call.go",
"managed_zones_list_call.go",
"managed_zones_list_response.go",
"managed_zones_service.go",
"rrset.go",
"rrsets_list_call.go",
"rrsets_list_response.go",
"rrsets_service.go",
"service.go",
],
tags = ["automanaged"],
deps = [
"//federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces:go_default_library",
"//federation/pkg/dnsprovider/rrstype:go_default_library",
"//pkg/util/uuid:go_default_library",
"//vendor:google.golang.org/api/dns/v1",
"//vendor:google.golang.org/api/googleapi",
],
)

View file

@ -0,0 +1,43 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.Change = Change{}
type Change struct{ impl *dns.Change }
func (c Change) Additions() (rrsets []interfaces.ResourceRecordSet) {
rrsets = make([]interfaces.ResourceRecordSet, len(c.impl.Additions))
for index, addition := range c.impl.Additions {
rrsets[index] = interfaces.ResourceRecordSet(&ResourceRecordSet{addition})
}
return rrsets
}
func (c Change) Deletions() (rrsets []interfaces.ResourceRecordSet) {
rrsets = make([]interfaces.ResourceRecordSet, len(c.impl.Deletions))
for index, deletion := range c.impl.Deletions {
rrsets[index] = interfaces.ResourceRecordSet(&ResourceRecordSet{deletion})
}
return rrsets
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ChangesCreateCall = ChangesCreateCall{}
type ChangesCreateCall struct{ impl *dns.ChangesCreateCall }
func (c ChangesCreateCall) Do(opts ...googleapi.CallOption) (interfaces.Change, error) {
ch, err := c.impl.Do(opts...)
return &Change{ch}, err
}

View file

@ -0,0 +1,43 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ChangesService = ChangesService{}
type ChangesService struct{ impl *dns.ChangesService }
func (c ChangesService) Create(project string, managedZone string, change interfaces.Change) interfaces.ChangesCreateCall {
return &ChangesCreateCall{c.impl.Create(project, managedZone, change.(*Change).impl)}
}
func (c ChangesService) NewChange(additions, deletions []interfaces.ResourceRecordSet) interfaces.Change {
adds := make([]*dns.ResourceRecordSet, len(additions))
deletes := make([]*dns.ResourceRecordSet, len(deletions))
for i, a := range additions {
adds[i] = a.(*ResourceRecordSet).impl
}
for i, d := range deletions {
deletes[i] = d.(*ResourceRecordSet).impl
}
return &Change{&dns.Change{Additions: adds, Deletions: deletes}}
}

View file

@ -0,0 +1,35 @@
/*
Copyright 2016 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 internal
// Implementation of internal/interfaces/* on top of Google Cloud DNS API.
// See https://godoc.org/google.golang.org/api/dns/v1 for details
// This facilitates stubbing out Google Cloud DNS for unit testing.
// Only the parts of the API that we use are included.
// Others can be added as needed.
import dns "google.golang.org/api/dns/v1"
type (
Project struct{ impl *dns.Project }
ProjectsGetCall struct{ impl *dns.ProjectsGetCall }
ProjectsService struct{ impl *dns.ProjectsService }
Quota struct{ impl *dns.Quota }
)

View file

@ -0,0 +1,21 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["interfaces.go"],
tags = ["automanaged"],
deps = [
"//federation/pkg/dnsprovider/rrstype:go_default_library",
"//vendor:google.golang.org/api/googleapi",
],
)

View file

@ -0,0 +1,208 @@
/*
Copyright 2016 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 interfaces
import (
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
)
// Interfaces to directly mirror the Google Cloud DNS API structures.
// See https://godoc.org/google.golang.org/api/dns/v1 for details
// This facilitates stubbing out Google Cloud DNS for unit testing.
// Only the parts of the API that we use are included.
// Others can be added as needed.
type (
Change interface {
Additions() []ResourceRecordSet
Deletions() []ResourceRecordSet
// Id() string // TODO: Add as needed
// Kind() string // TODO: Add as needed
// StartTime() string // TODO: Add as needed
// Status() string // TODO: Add as needed
}
ChangesCreateCall interface {
// Context(ctx context.Context) *ChangesCreateCall // TODO: Add as needed
Do(opts ...googleapi.CallOption) (Change, error)
// Fields(s ...googleapi.Field) *ChangesCreateCall // TODO: Add as needed
}
ChangesGetCall interface {
// Context(ctx context.Context) *ChangesGetCall // TODO: Add as needed
Do(opts ...googleapi.CallOption) (*Change, error)
// Fields(s ...googleapi.Field) *ChangesGetCall // TODO: Add as needed
// IfNoneMatch(entityTag string) *ChangesGetCall // TODO: Add as needed
}
ChangesListCall interface {
// Context(ctx context.Context) *ChangesListCall // TODO: Add as needed
Do(opts ...googleapi.CallOption) (*ChangesListResponse, error)
// Fields(s ...googleapi.Field) *ChangesListCall // TODO: Add as needed
// IfNoneMatch(entityTag string) *ChangesListCall // TODO: Add as needed
// MaxResults(maxResults int64) *ChangesListCall // TODO: Add as needed
// PageToken(pageToken string) *ChangesListCall // TODO: Add as needed
// Pages(ctx context.Context, f func(*ChangesListResponse) error) error // TODO: Add as needed
// SortBy(sortBy string) *ChangesListCall // TODO: Add as needed
// SortOrder(sortOrder string) *ChangesListCall // TODO: Add as needed
}
ChangesListResponse interface {
// Changes() []*Change // TODO: Add as needed
// Kind() string // TODO: Add as needed
// NextPageToken() string // TODO: Add as needed
// ServerResponse() googleapi.ServerResponse // TODO: Add as needed
// ForceSendFields() []string // TODO: Add as needed
}
ChangesService interface {
// Create(project string, managedZone string, change *Change) *ChangesCreateCall // TODO: Add as needed
Create(project string, managedZone string, change Change) ChangesCreateCall
NewChange(additions, deletions []ResourceRecordSet) Change
// Get(project string, managedZone string, changeId string) *ChangesGetCall // TODO: Add as needed
// List(project string, managedZone string) *ChangesListCall // TODO: Add as needed
}
ManagedZone interface {
// CreationTime() string // TODO: Add as needed
// Description() string // TODO: Add as needed
DnsName() string
Id() uint64
// Kind() string // TODO: Add as needed
Name() string
// NameServerSet() string // TODO: Add as needed
// NameServers() []string // TODO: Add as needed
// ServerResponse() googleapi.ServerResponse // TODO: Add as needed
// ForceSendFields() []string // TODO: Add as needed
}
ManagedZonesCreateCall interface {
// Context(ctx context.Context) *ManagedZonesCreateCall // TODO: Add as needed
Do(opts ...googleapi.CallOption) (ManagedZone, error)
// Fields(s ...googleapi.Field) *ManagedZonesCreateCall // TODO: Add as needed
}
ManagedZonesDeleteCall interface {
// Context(ctx context.Context) *ManagedZonesDeleteCall // TODO: Add as needed
Do(opts ...googleapi.CallOption) error
// Fields(s ...googleapi.Field) *ManagedZonesDeleteCall // TODO: Add as needed
}
ManagedZonesGetCall interface {
// Context(ctx context.Context) *ManagedZonesGetCall // TODO: Add as needed
Do(opts ...googleapi.CallOption) (ManagedZone, error)
// Fields(s ...googleapi.Field) *ManagedZonesGetCall // TODO: Add as needed
// IfNoneMatch(entityTag string) *ManagedZonesGetCall // TODO: Add as needed
}
ManagedZonesListCall interface {
// Context(ctx context.Context) *ManagedZonesListCall // TODO: Add as needed
DnsName(dnsName string) ManagedZonesListCall
Do(opts ...googleapi.CallOption) (ManagedZonesListResponse, error)
// Fields(s ...googleapi.Field) *ManagedZonesListCall // TODO: Add as needed
// IfNoneMatch(entityTag string) *ManagedZonesListCall // TODO: Add as needed
// MaxResults(maxResults int64) *ManagedZonesListCall // TODO: Add as needed
// PageToken(pageToken string) *ManagedZonesListCall // TODO: Add as needed
// Pages(ctx context.Context, f func(*ManagedZonesListResponse) error) error // TODO: Add as needed
}
ManagedZonesListResponse interface {
// Kind() string // TODO: Add as needed
// ManagedZones() []*ManagedZone // TODO: Add as needed
ManagedZones() []ManagedZone
// NextPageToken string // TODO: Add as needed
// ServerResponse() googleapi.ServerResponse // TODO: Add as needed
// ForceSendFields() []string // TODO: Add as needed
}
ManagedZonesService interface {
// NewManagedZonesService(s *Service) *ManagedZonesService // TODO: Add to service if needed
Create(project string, managedZone ManagedZone) ManagedZonesCreateCall
Delete(project string, managedZone string) ManagedZonesDeleteCall
Get(project string, managedZone string) ManagedZonesGetCall
List(project string) ManagedZonesListCall
NewManagedZone(dnsName string) ManagedZone
}
Project interface {
// Id() string // TODO: Add as needed
// Kind() string // TODO: Add as needed
// Number() uint64 // TODO: Add as needed
// Quota() *Quota // TODO: Add as needed
// ServerResponse() googleapi.ServerResponse // TODO: Add as needed
// ForceSendFields() []string // TODO: Add as needed
}
ProjectsGetCall interface {
// TODO: Add as needed
}
ProjectsService interface {
// TODO: Add as needed
}
Quota interface {
// TODO: Add as needed
}
ResourceRecordSet interface {
// Kind() string // TODO: Add as needed
Name() string
Rrdatas() []string
Ttl() int64
Type() string
// ForceSendFields []string // TODO: Add as needed
}
ResourceRecordSetsListCall interface {
// Context(ctx context.Context) *ResourceRecordSetsListCall // TODO: Add as needed
// Do(opts ...googleapi.CallOption) (*ResourceRecordSetsListResponse, error) // TODO: Add as needed
Do(opts ...googleapi.CallOption) (ResourceRecordSetsListResponse, error)
// Fields(s ...googleapi.Field) *ResourceRecordSetsListCall // TODO: Add as needed
// IfNoneMatch(entityTag string) *ResourceRecordSetsListCall // TODO: Add as needed
// MaxResults(maxResults int64) *ResourceRecordSetsListCall // TODO: Add as needed
Name(name string) ResourceRecordSetsListCall
// PageToken(pageToken string) *ResourceRecordSetsListCall // TODO: Add as needed
Type(type_ string) ResourceRecordSetsListCall
}
ResourceRecordSetsListResponse interface {
// Kind() string // TODO: Add as needed
// NextPageToken() string // TODO: Add as needed
Rrsets() []ResourceRecordSet
// ServerResponse() googleapi.ServerResponse // TODO: Add as needed
// ForceSendFields() []string // TODO: Add as needed
}
ResourceRecordSetsService interface {
// NewResourceRecordSetsService(s *Service) *ResourceRecordSetsService // TODO: add to service as needed
List(project string, managedZone string) ResourceRecordSetsListCall
NewResourceRecordSet(name string, rrdatas []string, ttl int64, type_ rrstype.RrsType) ResourceRecordSet
}
Service interface {
// BasePath() string // TODO: Add as needed
// UserAgent() string // TODO: Add as needed
Changes() ChangesService
ManagedZones() ManagedZonesService
Projects() ProjectsService
ResourceRecordSets() ResourceRecordSetsService
}
// New(client *http.Client) (*Service, error) // TODO: Add as needed
)

View file

@ -0,0 +1,39 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ManagedZone = ManagedZone{}
type ManagedZone struct{ impl *dns.ManagedZone }
func (m ManagedZone) Name() string {
return m.impl.Name
}
func (m ManagedZone) Id() uint64 {
return m.impl.Id
}
func (m ManagedZone) DnsName() string {
return m.impl.DnsName
}

View file

@ -0,0 +1,33 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesCreateCall = ManagedZonesCreateCall{}
type ManagedZonesCreateCall struct{ impl *dns.ManagedZonesCreateCall }
func (call ManagedZonesCreateCall) Do(opts ...googleapi.CallOption) (interfaces.ManagedZone, error) {
m, err := call.impl.Do(opts...)
return &ManagedZone{m}, err
}

View file

@ -0,0 +1,32 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesDeleteCall = ManagedZonesDeleteCall{}
type ManagedZonesDeleteCall struct{ impl *dns.ManagedZonesDeleteCall }
func (call ManagedZonesDeleteCall) Do(opts ...googleapi.CallOption) error {
return call.impl.Do(opts...)
}

View file

@ -0,0 +1,33 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesGetCall = ManagedZonesGetCall{}
type ManagedZonesGetCall struct{ impl *dns.ManagedZonesGetCall }
func (call ManagedZonesGetCall) Do(opts ...googleapi.CallOption) (interfaces.ManagedZone, error) {
m, err := call.impl.Do(opts...)
return &ManagedZone{m}, err
}

View file

@ -0,0 +1,38 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesListCall = &ManagedZonesListCall{}
type ManagedZonesListCall struct{ impl *dns.ManagedZonesListCall }
func (call *ManagedZonesListCall) Do(opts ...googleapi.CallOption) (interfaces.ManagedZonesListResponse, error) {
response, err := call.impl.Do(opts...)
return &ManagedZonesListResponse{response}, err
}
func (call *ManagedZonesListCall) DnsName(dnsName string) interfaces.ManagedZonesListCall {
call.impl.DnsName(dnsName)
return call
}

View file

@ -0,0 +1,35 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesListResponse = &ManagedZonesListResponse{}
type ManagedZonesListResponse struct{ impl *dns.ManagedZonesListResponse }
func (response *ManagedZonesListResponse) ManagedZones() []interfaces.ManagedZone {
zones := make([]interfaces.ManagedZone, len(response.impl.ManagedZones))
for i, z := range response.impl.ManagedZones {
zones[i] = &ManagedZone{z}
}
return zones
}

View file

@ -0,0 +1,51 @@
/*
Copyright 2016 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 internal
import (
"strings"
dns "google.golang.org/api/dns/v1"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
"k8s.io/kubernetes/pkg/util/uuid"
)
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesService = &ManagedZonesService{}
type ManagedZonesService struct{ impl *dns.ManagedZonesService }
func (m *ManagedZonesService) Create(project string, managedzone interfaces.ManagedZone) interfaces.ManagedZonesCreateCall {
return &ManagedZonesCreateCall{m.impl.Create(project, managedzone.(*ManagedZone).impl)}
}
func (m *ManagedZonesService) Delete(project, managedZone string) interfaces.ManagedZonesDeleteCall {
return &ManagedZonesDeleteCall{m.impl.Delete(project, managedZone)}
}
func (m *ManagedZonesService) Get(project, managedZone string) interfaces.ManagedZonesGetCall {
return &ManagedZonesGetCall{m.impl.Get(project, managedZone)}
}
func (m *ManagedZonesService) List(project string) interfaces.ManagedZonesListCall {
return &ManagedZonesListCall{m.impl.List(project)}
}
func (m *ManagedZonesService) NewManagedZone(dnsName string) interfaces.ManagedZone {
name := "x" + strings.Replace(string(uuid.NewUUID()), "-", "", -1)[0:30] // Unique name, strip out the "-" chars to shorten it, start with a lower case alpha, and truncate to Cloud DNS 32 character limit
return &ManagedZone{impl: &dns.ManagedZone{Name: name, Description: "Kubernetes Federated Service", DnsName: dnsName}}
}

View file

@ -0,0 +1,32 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ResourceRecordSet = ResourceRecordSet{}
type ResourceRecordSet struct{ impl *dns.ResourceRecordSet }
func (r ResourceRecordSet) Name() string { return r.impl.Name }
func (r ResourceRecordSet) Rrdatas() []string { return r.impl.Rrdatas }
func (r ResourceRecordSet) Ttl() int64 { return r.impl.Ttl }
func (r ResourceRecordSet) Type() string { return r.impl.Type }

View file

@ -0,0 +1,45 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ResourceRecordSetsListCall = &ResourceRecordSetsListCall{}
type ResourceRecordSetsListCall struct {
impl *dns.ResourceRecordSetsListCall
}
func (call *ResourceRecordSetsListCall) Do(opts ...googleapi.CallOption) (interfaces.ResourceRecordSetsListResponse, error) {
response, err := call.impl.Do(opts...)
return &ResourceRecordSetsListResponse{response}, err
}
func (call *ResourceRecordSetsListCall) Name(name string) interfaces.ResourceRecordSetsListCall {
call.impl.Name(name)
return call
}
func (call *ResourceRecordSetsListCall) Type(type_ string) interfaces.ResourceRecordSetsListCall {
call.impl.Type(type_)
return call
}

View file

@ -0,0 +1,38 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ResourceRecordSetsListResponse = &ResourceRecordSetsListResponse{}
type ResourceRecordSetsListResponse struct {
impl *dns.ResourceRecordSetsListResponse
}
func (response *ResourceRecordSetsListResponse) Rrsets() []interfaces.ResourceRecordSet {
rrsets := make([]interfaces.ResourceRecordSet, len(response.impl.Rrsets))
for i, rrset := range response.impl.Rrsets {
rrsets[i] = &ResourceRecordSet{rrset}
}
return rrsets
}

View file

@ -0,0 +1,39 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
)
// Compile time check for interface adeherence
var _ interfaces.ResourceRecordSetsService = &ResourceRecordSetsService{}
type ResourceRecordSetsService struct {
impl *dns.ResourceRecordSetsService
}
func (service ResourceRecordSetsService) List(project string, managedZone string) interfaces.ResourceRecordSetsListCall {
return &ResourceRecordSetsListCall{service.impl.List(project, managedZone)}
}
func (service ResourceRecordSetsService) NewResourceRecordSet(name string, rrdatas []string, ttl int64, type_ rrstype.RrsType) interfaces.ResourceRecordSet {
rrset := dns.ResourceRecordSet{Name: name, Rrdatas: rrdatas, Ttl: ttl, Type: string(type_)}
return &ResourceRecordSet{&rrset}
}

View file

@ -0,0 +1,49 @@
/*
Copyright 2016 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 internal
import (
dns "google.golang.org/api/dns/v1"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.Service = &Service{}
type Service struct {
impl *dns.Service
}
func NewService(service *dns.Service) *Service {
return &Service{service}
}
func (s *Service) Changes() interfaces.ChangesService {
return &ChangesService{s.impl.Changes}
}
func (s *Service) ManagedZones() interfaces.ManagedZonesService {
return &ManagedZonesService{s.impl.ManagedZones}
}
func (s *Service) Projects() interfaces.ProjectsService {
return &ProjectsService{s.impl.Projects}
}
func (s *Service) ResourceRecordSets() interfaces.ResourceRecordSetsService {
return &ResourceRecordSetsService{s.impl.ResourceRecordSets}
}

View file

@ -0,0 +1,40 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = [
"change.go",
"changes_create_call.go",
"changes_service.go",
"clouddns.go",
"managed_zone.go",
"managed_zone_create_call.go",
"managed_zones_delete_call.go",
"managed_zones_get_call.go",
"managed_zones_list_call.go",
"managed_zones_list_response.go",
"managed_zones_service.go",
"rrset.go",
"rrsets_list_call.go",
"rrsets_list_response.go",
"rrsets_service.go",
"service.go",
],
tags = ["automanaged"],
deps = [
"//federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces:go_default_library",
"//federation/pkg/dnsprovider/rrstype:go_default_library",
"//vendor:google.golang.org/api/dns/v1",
"//vendor:google.golang.org/api/googleapi",
],
)

View file

@ -0,0 +1,36 @@
/*
Copyright 2016 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 stubs
import "k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
// Compile time check for interface adeherence
var _ interfaces.Change = &Change{}
type Change struct {
Service *ChangesService
Additions_ []interfaces.ResourceRecordSet
Deletions_ []interfaces.ResourceRecordSet
}
func (c *Change) Additions() (rrsets []interfaces.ResourceRecordSet) {
return c.Additions_
}
func (c *Change) Deletions() (rrsets []interfaces.ResourceRecordSet) {
return c.Deletions_
}

View file

@ -0,0 +1,67 @@
/*
Copyright 2016 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 stubs
import (
"fmt"
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ChangesCreateCall = ChangesCreateCall{}
type ChangesCreateCall struct {
Service *ChangesService
Project string
Zone string
Change interfaces.Change
Error error // Use this to over-ride response if necessary
}
func hashKey(set interfaces.ResourceRecordSet) string {
return fmt.Sprintf("%s-%d-%s", set.Name(), set.Ttl(), string(set.Type()))
}
func (c ChangesCreateCall) Do(opts ...googleapi.CallOption) (interfaces.Change, error) {
if c.Error != nil {
return nil, c.Error
}
zone := (c.Service.Service.ManagedZones_.Impl[c.Project][c.Zone]).(*ManagedZone)
rrsets := map[string]ResourceRecordSet{} // compute the new state
for _, set := range zone.Rrsets {
rrsets[hashKey(set)] = set
}
for _, del := range c.Change.Deletions() {
if _, found := rrsets[hashKey(del)]; !found {
return nil, fmt.Errorf("Attempt to delete non-existent rrset %v", del)
}
delete(rrsets, hashKey(del))
}
for _, add := range c.Change.Additions() {
if _, found := rrsets[hashKey(add)]; found {
return nil, fmt.Errorf("Attempt to insert duplicate rrset %v", add)
}
rrsets[hashKey(add)] = add.(ResourceRecordSet)
}
zone.Rrsets = []ResourceRecordSet{}
for _, rrset := range rrsets {
zone.Rrsets = append(zone.Rrsets, rrset)
}
return c.Change, nil
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2016 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 stubs
import "k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
// Compile time check for interface adeherence
var _ interfaces.ChangesService = &ChangesService{}
type ChangesService struct {
Service *Service
}
func (c *ChangesService) Create(project string, managedZone string, change interfaces.Change) interfaces.ChangesCreateCall {
return &ChangesCreateCall{c, project, managedZone, change, nil}
}
func (c *ChangesService) NewChange(additions, deletions []interfaces.ResourceRecordSet) interfaces.Change {
return &Change{c, additions, deletions}
}

View file

@ -0,0 +1,33 @@
/*
Copyright 2016 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 stubs
// Implementation of internal/interfaces/* on top of Google Cloud DNS API.
// See https://godoc.org/google.golang.org/api/dns/v1 for details
// This facilitates stubbing out Google Cloud DNS for unit testing.
// Only the parts of the API that we use are included.
// Others can be added as needed.
import dns "google.golang.org/api/dns/v1"
type (
// TODO: We don't need these yet, so they remain unimplemented. Add later as required.
Project struct{ impl *dns.Project }
ProjectsGetCall struct{ impl *dns.ProjectsGetCall }
ProjectsService struct{ impl *dns.ProjectsService }
Quota struct{ impl *dns.Quota }
)

View file

@ -0,0 +1,41 @@
/*
Copyright 2016 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 stubs
import "k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
// Compile time check for interface adeherence
var _ interfaces.ManagedZone = ManagedZone{}
type ManagedZone struct {
Service *ManagedZonesService
Name_ string
Id_ uint64
Rrsets []ResourceRecordSet
}
func (m ManagedZone) Name() string {
return m.Name_
}
func (m ManagedZone) Id() uint64 {
return m.Id_
}
func (m ManagedZone) DnsName() string {
return m.Name_ // Don't bother storing a separate DNS name
}

View file

@ -0,0 +1,52 @@
/*
Copyright 2016 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 stubs
import (
"fmt"
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesCreateCall = ManagedZonesCreateCall{}
type ManagedZonesCreateCall struct {
Error *error // Use to override response for testing
Service *ManagedZonesService
Project string
ManagedZone interfaces.ManagedZone
}
func (call ManagedZonesCreateCall) Do(opts ...googleapi.CallOption) (interfaces.ManagedZone, error) {
if call.Error != nil {
return nil, *call.Error
}
if call.Service.Impl[call.Project][call.ManagedZone.DnsName()] != nil {
return nil, fmt.Errorf("Error - attempt to create duplicate zone %s in project %s.",
call.ManagedZone.DnsName(), call.Project)
}
if call.Service.Impl == nil {
call.Service.Impl = map[string]map[string]interfaces.ManagedZone{}
}
if call.Service.Impl[call.Project] == nil {
call.Service.Impl[call.Project] = map[string]interfaces.ManagedZone{}
}
call.Service.Impl[call.Project][call.ManagedZone.DnsName()] = call.ManagedZone
return call.ManagedZone, nil
}

View file

@ -0,0 +1,53 @@
/*
Copyright 2016 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 stubs
import (
"fmt"
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesDeleteCall = ManagedZonesDeleteCall{}
type ManagedZonesDeleteCall struct {
Service *ManagedZonesService
Project string
ZoneName string
Error *error // Use this to override response for testing if required
}
func (call ManagedZonesDeleteCall) Do(opts ...googleapi.CallOption) error {
if call.Error != nil { // Return the override value
return *call.Error
} else { // Just try to delete it from the in-memory array.
project, ok := call.Service.Impl[call.Project]
if ok {
zone, ok := project[call.ZoneName]
if ok {
delete(project, zone.Name())
return nil
} else {
return fmt.Errorf("Failed to find zone %s in project %s to delete it", call.ZoneName, call.Project)
}
} else {
return fmt.Errorf("Failed to find project %s to delete zone %s from it", call.Project, call.ZoneName)
}
}
}

View file

@ -0,0 +1,42 @@
/*
Copyright 2016 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 stubs
import (
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesGetCall = ManagedZonesGetCall{}
type ManagedZonesGetCall struct {
Service *ManagedZonesService
Project string
ZoneName string
Response interfaces.ManagedZone // Use this to override response if required
Error *error // Use this to override response if required
DnsName_ string
}
func (call ManagedZonesGetCall) Do(opts ...googleapi.CallOption) (interfaces.ManagedZone, error) {
if call.Response != nil {
return call.Response, *call.Error
} else {
return call.Service.Impl[call.Project][call.ZoneName], nil
}
}

View file

@ -0,0 +1,59 @@
/*
Copyright 2016 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 stubs
import (
"fmt"
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesListCall = &ManagedZonesListCall{}
type ManagedZonesListCall struct {
Service *ManagedZonesService
Project string
Response *interfaces.ManagedZonesListResponse // Use this to override response if required
Error *error // Use this to override response if required
DnsName_ string
}
func (call *ManagedZonesListCall) Do(opts ...googleapi.CallOption) (interfaces.ManagedZonesListResponse, error) {
if call.Response != nil {
return *call.Response, *call.Error
} else {
proj, projectFound := call.Service.Impl[call.Project]
if !projectFound {
return nil, fmt.Errorf("Project %s not found.", call.Project)
}
if call.DnsName_ != "" {
return &ManagedZonesListResponse{[]interfaces.ManagedZone{proj[call.DnsName_]}}, nil
}
list := []interfaces.ManagedZone{}
for _, zone := range proj {
list = append(list, zone)
}
return &ManagedZonesListResponse{list}, nil
}
}
func (call *ManagedZonesListCall) DnsName(dnsName string) interfaces.ManagedZonesListCall {
call.DnsName_ = dnsName
return call
}

View file

@ -0,0 +1,28 @@
/*
Copyright 2016 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 stubs
import "k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesListResponse = &ManagedZonesListResponse{}
type ManagedZonesListResponse struct{ ManagedZones_ []interfaces.ManagedZone }
func (response *ManagedZonesListResponse) ManagedZones() []interfaces.ManagedZone {
return response.ManagedZones_
}

View file

@ -0,0 +1,46 @@
/*
Copyright 2016 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 stubs
import "k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
// Compile time check for interface adeherence
var _ interfaces.ManagedZonesService = &ManagedZonesService{}
type ManagedZonesService struct {
Impl map[string]map[string]interfaces.ManagedZone
}
func (m *ManagedZonesService) Create(project string, managedzone interfaces.ManagedZone) interfaces.ManagedZonesCreateCall {
return &ManagedZonesCreateCall{nil, m, project, managedzone.(*ManagedZone)}
}
func (m *ManagedZonesService) Delete(project string, managedZone string) interfaces.ManagedZonesDeleteCall {
return &ManagedZonesDeleteCall{m, project, managedZone, nil}
}
func (m *ManagedZonesService) Get(project string, managedZone string) interfaces.ManagedZonesGetCall {
return &ManagedZonesGetCall{m, project, managedZone, nil, nil, ""}
}
func (m *ManagedZonesService) List(project string) interfaces.ManagedZonesListCall {
return &ManagedZonesListCall{m, project, nil, nil, ""}
}
func (m *ManagedZonesService) NewManagedZone(dnsName string) interfaces.ManagedZone {
return &ManagedZone{Service: m, Name_: dnsName}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2016 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 stubs
import "k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
// Compile time check for interface adeherence
var _ interfaces.ResourceRecordSet = ResourceRecordSet{}
type ResourceRecordSet struct {
Name_ string
Rrdatas_ []string
Ttl_ int64
Type_ string
}
func (r ResourceRecordSet) Name() string { return r.Name_ }
func (r ResourceRecordSet) Rrdatas() []string { return r.Rrdatas_ }
func (r ResourceRecordSet) Ttl() int64 { return r.Ttl_ }
func (r ResourceRecordSet) Type() string { return r.Type_ }

View file

@ -0,0 +1,46 @@
/*
Copyright 2016 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 stubs
import (
"google.golang.org/api/googleapi"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ interfaces.ResourceRecordSetsListCall = &ResourceRecordSetsListCall{}
type ResourceRecordSetsListCall struct {
Response_ *ResourceRecordSetsListResponse
Err_ error
Name_ string
Type_ string
}
func (call *ResourceRecordSetsListCall) Do(opts ...googleapi.CallOption) (interfaces.ResourceRecordSetsListResponse, error) {
return call.Response_, call.Err_
}
func (call *ResourceRecordSetsListCall) Name(name string) interfaces.ResourceRecordSetsListCall {
call.Name_ = name
return call
}
func (call *ResourceRecordSetsListCall) Type(type_ string) interfaces.ResourceRecordSetsListCall {
call.Type_ = type_
return call
}

View file

@ -0,0 +1,30 @@
/*
Copyright 2016 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 stubs
import "k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
// Compile time check for interface adeherence
var _ interfaces.ResourceRecordSetsListResponse = &ResourceRecordSetsListResponse{}
type ResourceRecordSetsListResponse struct {
impl []interfaces.ResourceRecordSet
}
func (response *ResourceRecordSetsListResponse) Rrsets() []interfaces.ResourceRecordSet {
return response.impl
}

View file

@ -0,0 +1,59 @@
/*
Copyright 2016 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 stubs
import (
"fmt"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
)
// Compile time check for interface adeherence
var _ interfaces.ResourceRecordSetsService = &ResourceRecordSetsService{}
type ResourceRecordSetsService struct {
Service *Service
ListCall interfaces.ResourceRecordSetsListCall // Use to override response if reqired for testing
}
func (s ResourceRecordSetsService) List(project string, managedZone string) interfaces.ResourceRecordSetsListCall {
if s.ListCall != nil {
return s.ListCall
}
p := s.Service.ManagedZones_.Impl[project]
if p == nil {
return &ResourceRecordSetsListCall{Err_: fmt.Errorf("Project not found: %s", project)}
}
z := s.Service.ManagedZones_.Impl[project][managedZone]
if z == nil {
return &ResourceRecordSetsListCall{
Err_: fmt.Errorf("Zone %s not found in project %s", managedZone, project),
}
}
zone := s.Service.ManagedZones_.Impl[project][managedZone].(*ManagedZone)
response := &ResourceRecordSetsListResponse{}
for _, set := range zone.Rrsets {
response.impl = append(response.impl, set)
}
return &ResourceRecordSetsListCall{Response_: response}
}
func (service ResourceRecordSetsService) NewResourceRecordSet(name string, rrdatas []string, ttl int64, type_ rrstype.RrsType) interfaces.ResourceRecordSet {
rrset := ResourceRecordSet{Name_: name, Rrdatas_: rrdatas, Ttl_: ttl, Type_: string(type_)}
return rrset
}

View file

@ -0,0 +1,54 @@
/*
Copyright 2016 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 stubs
import "k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
// Compile time check for interface adeherence
var _ interfaces.Service = &Service{}
type Service struct {
Changes_ *ChangesService
ManagedZones_ *ManagedZonesService
Projects_ *ProjectsService
Rrsets_ *ResourceRecordSetsService
}
func NewService() *Service {
s := &Service{}
s.Changes_ = &ChangesService{s}
s.ManagedZones_ = &ManagedZonesService{}
s.Projects_ = &ProjectsService{}
s.Rrsets_ = &ResourceRecordSetsService{s, nil}
return s
}
func (s *Service) Changes() interfaces.ChangesService {
return s.Changes_
}
func (s *Service) ManagedZones() interfaces.ManagedZonesService {
return s.ManagedZones_
}
func (s *Service) Projects() interfaces.ProjectsService {
return s.Projects_
}
func (s *Service) ResourceRecordSets() interfaces.ResourceRecordSetsService {
return s.Rrsets_
}

View file

@ -0,0 +1,74 @@
/*
Copyright 2016 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 clouddns
import (
"fmt"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adherence
var _ dnsprovider.ResourceRecordChangeset = &ResourceRecordChangeset{}
type ResourceRecordChangeset struct {
rrsets *ResourceRecordSets
additions []dnsprovider.ResourceRecordSet
removals []dnsprovider.ResourceRecordSet
}
func (c *ResourceRecordChangeset) Add(rrset dnsprovider.ResourceRecordSet) dnsprovider.ResourceRecordChangeset {
c.additions = append(c.additions, rrset)
return c
}
func (c *ResourceRecordChangeset) Remove(rrset dnsprovider.ResourceRecordSet) dnsprovider.ResourceRecordChangeset {
c.removals = append(c.removals, rrset)
return c
}
func (c *ResourceRecordChangeset) Apply() error {
rrsets := c.rrsets
service := rrsets.zone.zones.interface_.service.Changes()
var additions []interfaces.ResourceRecordSet
for _, r := range c.additions {
additions = append(additions, r.(ResourceRecordSet).impl)
}
var deletions []interfaces.ResourceRecordSet
for _, r := range c.removals {
deletions = append(deletions, r.(ResourceRecordSet).impl)
}
change := service.NewChange(additions, deletions)
newChange, err := service.Create(rrsets.project(), rrsets.zone.impl.Name(), change).Do()
if err != nil {
return err
}
newAdditions := newChange.Additions()
if len(newAdditions) != len(additions) {
return fmt.Errorf("Internal error when adding resource record set. Call succeeded but number of records returned is incorrect. Records sent=%d, records returned=%d, additions:%v", len(additions), len(newAdditions), c.additions)
}
newDeletions := newChange.Deletions()
if len(newDeletions) != len(deletions) {
return fmt.Errorf("Internal error when deleting resource record set. Call succeeded but number of records returned is incorrect. Records sent=%d, records returned=%d, deletions:%v", len(deletions), len(newDeletions), c.removals)
}
return nil
}

View file

@ -0,0 +1,53 @@
/*
Copyright 2016 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 clouddns
import (
"fmt"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
)
// Compile time check for interface adeherence
var _ dnsprovider.ResourceRecordSet = ResourceRecordSet{}
type ResourceRecordSet struct {
impl interfaces.ResourceRecordSet
rrsets *ResourceRecordSets
}
func (rrset ResourceRecordSet) String() string {
return fmt.Sprintf("<(clouddns) %q type=%s rrdatas=%q ttl=%v>", rrset.Name(), rrset.Type(), rrset.Rrdatas(), rrset.Ttl())
}
func (rrset ResourceRecordSet) Name() string {
return rrset.impl.Name()
}
func (rrset ResourceRecordSet) Rrdatas() []string {
return rrset.impl.Rrdatas()
}
func (rrset ResourceRecordSet) Ttl() int64 {
return rrset.impl.Ttl()
}
func (rrset ResourceRecordSet) Type() rrstype.RrsType {
return rrstype.RrsType(rrset.impl.Type())
}

View file

@ -0,0 +1,57 @@
/*
Copyright 2016 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 clouddns
import (
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
)
// Compile time check for interface adherence
var _ dnsprovider.ResourceRecordSets = ResourceRecordSets{}
type ResourceRecordSets struct {
zone *Zone
impl interfaces.ResourceRecordSetsService
}
func (rrsets ResourceRecordSets) List() ([]dnsprovider.ResourceRecordSet, error) {
response, err := rrsets.impl.List(rrsets.project(), rrsets.zone.impl.Name()).Do()
if err != nil {
return nil, err
}
list := make([]dnsprovider.ResourceRecordSet, len(response.Rrsets()))
for i, rrset := range response.Rrsets() {
list[i] = ResourceRecordSet{rrset, &rrsets}
}
return list, nil
}
func (r ResourceRecordSets) StartChangeset() dnsprovider.ResourceRecordChangeset {
return &ResourceRecordChangeset{
rrsets: &r,
}
}
func (r ResourceRecordSets) New(name string, rrdatas []string, ttl int64, rrstype rrstype.RrsType) dnsprovider.ResourceRecordSet {
return ResourceRecordSet{r.impl.NewResourceRecordSet(name, rrdatas, ttl, rrstype), &r}
}
func (rrsets ResourceRecordSets) project() string {
return rrsets.zone.project()
}

View file

@ -0,0 +1,48 @@
/*
Copyright 2016 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 clouddns
import (
"strconv"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ dnsprovider.Zone = &Zone{}
type Zone struct {
impl interfaces.ManagedZone
zones *Zones
}
func (zone *Zone) Name() string {
return zone.impl.DnsName()
}
func (zone *Zone) ID() string {
return strconv.FormatUint(zone.impl.Id(), 10)
}
func (zone *Zone) ResourceRecordSets() (dnsprovider.ResourceRecordSets, bool) {
return &ResourceRecordSets{zone, zone.zones.interface_.service.ResourceRecordSets()}, true
}
func (zone Zone) project() string {
return zone.zones.project()
}

View file

@ -0,0 +1,68 @@
/*
Copyright 2016 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 clouddns
import (
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns/internal/interfaces"
)
// Compile time check for interface adeherence
var _ dnsprovider.Zones = Zones{}
type Zones struct {
impl interfaces.ManagedZonesService
interface_ *Interface
}
func (zones Zones) List() ([]dnsprovider.Zone, error) {
response, err := zones.impl.List(zones.project()).Do()
if err != nil {
return nil, err
}
managedZones := response.ManagedZones()
zoneList := make([]dnsprovider.Zone, len(managedZones))
for i, zone := range managedZones {
zoneList[i] = &Zone{zone, &zones}
}
return zoneList, nil
}
func (zones Zones) Add(zone dnsprovider.Zone) (dnsprovider.Zone, error) {
managedZone := zones.impl.NewManagedZone(zone.Name())
response, err := zones.impl.Create(zones.project(), managedZone).Do()
if err != nil {
return nil, err
}
return &Zone{response, &zones}, nil
}
func (zones Zones) Remove(zone dnsprovider.Zone) error {
if err := zones.impl.Delete(zones.project(), zone.(*Zone).impl.Name()).Do(); err != nil {
return err
}
return nil
}
func (zones Zones) New(name string) (dnsprovider.Zone, error) {
managedZone := zones.impl.NewManagedZone(name)
return &Zone{managedZone, &zones}, nil
}
func (zones Zones) project() string {
return zones.interface_.project()
}

View file

@ -0,0 +1,17 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["rrstype.go"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,28 @@
/*
Copyright 2016 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 rrstype
type (
RrsType string
)
const (
A = RrsType("A")
AAAA = RrsType("AAAA")
CNAME = RrsType("CNAME")
// TODO: Add other types as required
)

View file

@ -0,0 +1,21 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["commontests.go"],
tags = ["automanaged"],
deps = [
"//federation/pkg/dnsprovider:go_default_library",
"//federation/pkg/dnsprovider/rrstype:go_default_library",
],
)

View file

@ -0,0 +1,184 @@
/*
Copyright 2016 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 tests
import (
"reflect"
"testing"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
)
/* CommonTestResourceRecordSetsReplace verifies that replacing an RRS works */
func CommonTestResourceRecordSetsReplace(t *testing.T, zone dnsprovider.Zone) {
rrsets, _ := zone.ResourceRecordSets()
sets := rrs(t, zone)
rrset := rrsets.New("alpha.test.com", []string{"8.8.4.4"}, 40, rrstype.A)
addRrsetOrFail(t, sets, rrset)
defer sets.StartChangeset().Remove(rrset).Apply()
t.Logf("Successfully added resource record set: %v", rrset)
// Replace the record (change ttl and rrdatas)
newRrset := rrsets.New("alpha.test.com", []string{"8.8.8.8"}, 80, rrstype.A)
err := sets.StartChangeset().Add(newRrset).Remove(rrset).Apply()
if err != nil {
t.Errorf("Failed to replace resource record set %v -> %v: %v", rrset, newRrset, err)
} else {
t.Logf("Correctly replaced resource record %v -> %v", rrset, newRrset)
}
defer sets.StartChangeset().Remove(newRrset).Apply()
// Check that the record was updated
assertHasRecord(t, sets, newRrset)
}
/* CommonTestResourceRecordSetsReplaceAll verifies that we can remove an RRS and create one with a different name*/
func CommonTestResourceRecordSetsReplaceAll(t *testing.T, zone dnsprovider.Zone) {
rrsets, _ := zone.ResourceRecordSets()
sets := rrs(t, zone)
rrset := rrsets.New("alpha.test.com", []string{"8.8.4.4"}, 40, rrstype.A)
addRrsetOrFail(t, sets, rrset)
defer sets.StartChangeset().Remove(rrset).Apply()
t.Logf("Successfully added resource record set: %v", rrset)
newRrset := rrsets.New("beta.test.com", []string{"8.8.8.8"}, 80, rrstype.A)
// Try to add it again, and verify that the call fails.
err := sets.StartChangeset().Add(newRrset).Remove(rrset).Apply()
if err != nil {
t.Errorf("Failed to replace resource record set %v -> %v: %v", rrset, newRrset, err)
} else {
defer sets.StartChangeset().Remove(newRrset).Apply()
t.Logf("Correctly replaced resource record %v -> %v", rrset, newRrset)
}
// Check that it was updated
assertHasRecord(t, sets, newRrset)
assertNotHasRecord(t, sets, rrset.Name(), rrset.Type())
}
/* CommonTestResourceRecordSetsHonorsType verifies that we can add records of the same name but different types */
func CommonTestResourceRecordSetsDifferentTypes(t *testing.T, zone dnsprovider.Zone) {
rrsets, _ := zone.ResourceRecordSets()
sets := rrs(t, zone)
rrset := rrsets.New("alpha.test.com", []string{"8.8.4.4"}, 40, rrstype.A)
addRrsetOrFail(t, sets, rrset)
defer sets.StartChangeset().Remove(rrset).Apply()
t.Logf("Successfully added resource record set: %v", rrset)
aaaaRrset := rrsets.New("alpha.test.com", []string{"2001:4860:4860::8888"}, 80, rrstype.AAAA)
// Add the resource with the same name but different type
err := sets.StartChangeset().Add(aaaaRrset).Apply()
if err != nil {
t.Errorf("Failed to add resource record set %v: %v", aaaaRrset, err)
}
defer sets.StartChangeset().Remove(aaaaRrset).Apply()
// Check that both records exist
assertHasRecord(t, sets, aaaaRrset)
assertHasRecord(t, sets, rrset)
}
/* rrs returns the ResourceRecordSets interface for a given zone */
func rrs(t *testing.T, zone dnsprovider.Zone) (r dnsprovider.ResourceRecordSets) {
rrsets, supported := zone.ResourceRecordSets()
if !supported {
t.Fatalf("ResourceRecordSets interface not supported by zone %v", zone)
return r
}
return rrsets
}
func listRrsOrFail(t *testing.T, rrsets dnsprovider.ResourceRecordSets) []dnsprovider.ResourceRecordSet {
rrset, err := rrsets.List()
if err != nil {
t.Fatalf("Failed to list recordsets: %v", err)
} else {
if len(rrset) < 0 {
t.Fatalf("Record set length=%d, expected >=0", len(rrset))
} else {
t.Logf("Got %d recordsets: %v", len(rrset), rrset)
}
}
return rrset
}
// assertHasRecord tests that rrsets has a record equivalent to rrset
func assertHasRecord(t *testing.T, rrsets dnsprovider.ResourceRecordSets, rrset dnsprovider.ResourceRecordSet) {
var found dnsprovider.ResourceRecordSet
for _, r := range listRrsOrFail(t, rrsets) {
if r.Name() != rrset.Name() || r.Type() != rrset.Type() {
continue
}
if found != nil {
t.Errorf("found duplicate resource record set: %q and %q", r, found)
}
found = r
}
if found == nil {
t.Errorf("resource record set %v not found", rrset)
} else {
assertEquivalent(t, found, rrset)
}
}
// assertNotHasRecord tests that rrsets does not have a record matching name and type
func assertNotHasRecord(t *testing.T, rrsets dnsprovider.ResourceRecordSets, name string, rrstype rrstype.RrsType) {
var found dnsprovider.ResourceRecordSet
for _, r := range listRrsOrFail(t, rrsets) {
if r.Name() != name || r.Type() != rrstype {
continue
}
if found != nil {
t.Errorf("found duplicate resource record set: %q and %q", r, found)
}
found = r
}
if found != nil {
t.Errorf("resource record set found unexpectedly: %v", found)
}
}
// assertEquivalent tests that l is equal to r, for the methods in ResourceRecordSet
func assertEquivalent(t *testing.T, l, r dnsprovider.ResourceRecordSet) {
if l.Name() != r.Name() {
t.Errorf("resource record sets not equal %v vs %v", l, r)
}
if l.Type() != r.Type() {
t.Errorf("resource record sets not equal %v vs %v", l, r)
}
if l.Ttl() != r.Ttl() {
t.Errorf("resource record sets not equal %v vs %v", l, r)
}
if !reflect.DeepEqual(l.Rrdatas(), r.Rrdatas()) {
t.Errorf("resource record sets not equal %v vs %v", l, r)
}
}
func addRrsetOrFail(t *testing.T, rrsets dnsprovider.ResourceRecordSets, rrset dnsprovider.ResourceRecordSet) {
err := rrsets.StartChangeset().Add(rrset).Apply()
if err != nil {
t.Fatalf("Failed to add recordsets: %v", err)
}
}

View file

@ -0,0 +1,17 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["doc.go"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,4 @@
assignees:
- quinton-hoole
- nikhiljindal
- madhusudancs

View file

@ -0,0 +1,60 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = [
"cluster_client.go",
"clustercontroller.go",
"doc.go",
],
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/cache:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/restclient:go_default_library",
"//pkg/client/typed/discovery:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/util/runtime:go_default_library",
"//pkg/util/sets:go_default_library",
"//pkg/util/wait:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = ["clustercontroller_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//pkg/api/testapi:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/client/restclient:go_default_library",
"//pkg/client/unversioned/clientcmd:go_default_library",
"//pkg/client/unversioned/clientcmd/api:go_default_library",
"//pkg/util/uuid:go_default_library",
],
)

View file

@ -0,0 +1,169 @@
/*
Copyright 2016 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 cluster
import (
"fmt"
"strings"
"github.com/golang/glog"
federation_v1beta1 "k8s.io/kubernetes/federation/apis/federation/v1beta1"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/typed/discovery"
"k8s.io/kubernetes/pkg/util/sets"
)
const (
UserAgentName = "Cluster-Controller"
KubeAPIQPS = 20.0
KubeAPIBurst = 30
KubeconfigSecretDataKey = "kubeconfig"
)
type ClusterClient struct {
discoveryClient *discovery.DiscoveryClient
kubeClient *clientset.Clientset
}
func NewClusterClientSet(c *federation_v1beta1.Cluster) (*ClusterClient, error) {
clusterConfig, err := util.BuildClusterConfig(c)
if err != nil {
return nil, err
}
var clusterClientSet = ClusterClient{}
if clusterConfig != nil {
clusterClientSet.discoveryClient = discovery.NewDiscoveryClientForConfigOrDie((restclient.AddUserAgent(clusterConfig, UserAgentName)))
if clusterClientSet.discoveryClient == nil {
return nil, nil
}
clusterClientSet.kubeClient = clientset.NewForConfigOrDie((restclient.AddUserAgent(clusterConfig, UserAgentName)))
if clusterClientSet.kubeClient == nil {
return nil, nil
}
}
return &clusterClientSet, nil
}
// GetClusterHealthStatus gets the kubernetes cluster health status by requesting "/healthz"
func (self *ClusterClient) GetClusterHealthStatus() *federation_v1beta1.ClusterStatus {
clusterStatus := federation_v1beta1.ClusterStatus{}
currentTime := metav1.Now()
newClusterReadyCondition := federation_v1beta1.ClusterCondition{
Type: federation_v1beta1.ClusterReady,
Status: v1.ConditionTrue,
Reason: "ClusterReady",
Message: "/healthz responded with ok",
LastProbeTime: currentTime,
LastTransitionTime: currentTime,
}
newClusterNotReadyCondition := federation_v1beta1.ClusterCondition{
Type: federation_v1beta1.ClusterReady,
Status: v1.ConditionFalse,
Reason: "ClusterNotReady",
Message: "/healthz responded without ok",
LastProbeTime: currentTime,
LastTransitionTime: currentTime,
}
newNodeOfflineCondition := federation_v1beta1.ClusterCondition{
Type: federation_v1beta1.ClusterOffline,
Status: v1.ConditionTrue,
Reason: "ClusterNotReachable",
Message: "cluster is not reachable",
LastProbeTime: currentTime,
LastTransitionTime: currentTime,
}
newNodeNotOfflineCondition := federation_v1beta1.ClusterCondition{
Type: federation_v1beta1.ClusterOffline,
Status: v1.ConditionFalse,
Reason: "ClusterReachable",
Message: "cluster is reachable",
LastProbeTime: currentTime,
LastTransitionTime: currentTime,
}
body, err := self.discoveryClient.RESTClient().Get().AbsPath("/healthz").Do().Raw()
if err != nil {
clusterStatus.Conditions = append(clusterStatus.Conditions, newNodeOfflineCondition)
} else {
if !strings.EqualFold(string(body), "ok") {
clusterStatus.Conditions = append(clusterStatus.Conditions, newClusterNotReadyCondition, newNodeNotOfflineCondition)
} else {
clusterStatus.Conditions = append(clusterStatus.Conditions, newClusterReadyCondition)
}
}
return &clusterStatus
}
// GetClusterZones gets the kubernetes cluster zones and region by inspecting labels on nodes in the cluster.
func (self *ClusterClient) GetClusterZones() (zones []string, region string, err error) {
return getZoneNames(self.kubeClient)
}
// Find the name of the zone in which a Node is running
func getZoneNameForNode(node api.Node) (string, error) {
for key, value := range node.Labels {
if key == metav1.LabelZoneFailureDomain {
return value, nil
}
}
return "", fmt.Errorf("Zone name for node %s not found. No label with key %s",
node.Name, metav1.LabelZoneFailureDomain)
}
// Find the name of the region in which a Node is running
func getRegionNameForNode(node api.Node) (string, error) {
for key, value := range node.Labels {
if key == metav1.LabelZoneRegion {
return value, nil
}
}
return "", fmt.Errorf("Region name for node %s not found. No label with key %s",
node.Name, metav1.LabelZoneRegion)
}
// Find the names of all zones and the region in which we have nodes in this cluster.
func getZoneNames(client *clientset.Clientset) (zones []string, region string, err error) {
zoneNames := sets.NewString()
nodes, err := client.Core().Nodes().List(api.ListOptions{})
if err != nil {
glog.Errorf("Failed to list nodes while getting zone names: %v", err)
return nil, "", err
}
for i, node := range nodes.Items {
// TODO: quinton-hoole make this more efficient.
// For non-multi-zone clusters the zone will
// be identical for all nodes, so we only need to look at one node
// For multi-zone clusters we know at build time
// which zones are included. Rather get this info from there, because it's cheaper.
zoneName, err := getZoneNameForNode(node)
if err != nil {
return nil, "", err
}
zoneNames.Insert(zoneName)
if i == 0 {
region, err = getRegionNameForNode(node)
if err != nil {
return nil, "", err
}
}
}
return zoneNames.List(), region, nil
}

View file

@ -0,0 +1,209 @@
/*
Copyright 2016 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 cluster
import (
"strings"
"time"
"github.com/golang/glog"
federationv1beta1 "k8s.io/kubernetes/federation/apis/federation/v1beta1"
clustercache "k8s.io/kubernetes/federation/client/cache"
federationclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5"
"k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/client/cache"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/runtime"
utilruntime "k8s.io/kubernetes/pkg/util/runtime"
"k8s.io/kubernetes/pkg/util/sets"
"k8s.io/kubernetes/pkg/util/wait"
"k8s.io/kubernetes/pkg/watch"
)
type ClusterController struct {
knownClusterSet sets.String
// federationClient used to operate cluster
federationClient federationclientset.Interface
// clusterMonitorPeriod is the period for updating status of cluster
clusterMonitorPeriod time.Duration
// clusterClusterStatusMap is a mapping of clusterName and cluster status of last sampling
clusterClusterStatusMap map[string]federationv1beta1.ClusterStatus
// clusterKubeClientMap is a mapping of clusterName and restclient
clusterKubeClientMap map[string]ClusterClient
// cluster framework and store
clusterController *cache.Controller
clusterStore clustercache.StoreToClusterLister
}
// NewclusterController returns a new cluster controller
func NewclusterController(federationClient federationclientset.Interface, clusterMonitorPeriod time.Duration) *ClusterController {
cc := &ClusterController{
knownClusterSet: make(sets.String),
federationClient: federationClient,
clusterMonitorPeriod: clusterMonitorPeriod,
clusterClusterStatusMap: make(map[string]federationv1beta1.ClusterStatus),
clusterKubeClientMap: make(map[string]ClusterClient),
}
cc.clusterStore.Store, cc.clusterController = cache.NewInformer(
&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
return cc.federationClient.Federation().Clusters().List(options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
return cc.federationClient.Federation().Clusters().Watch(options)
},
},
&federationv1beta1.Cluster{},
controller.NoResyncPeriodFunc(),
cache.ResourceEventHandlerFuncs{
DeleteFunc: cc.delFromClusterSet,
AddFunc: cc.addToClusterSet,
},
)
return cc
}
// delFromClusterSet delete a cluster from clusterSet and
// delete the corresponding restclient from the map clusterKubeClientMap
func (cc *ClusterController) delFromClusterSet(obj interface{}) {
cluster := obj.(*federationv1beta1.Cluster)
cc.knownClusterSet.Delete(cluster.Name)
delete(cc.clusterKubeClientMap, cluster.Name)
}
// addToClusterSet insert the new cluster to clusterSet and create a corresponding
// restclient to map clusterKubeClientMap
func (cc *ClusterController) addToClusterSet(obj interface{}) {
cluster := obj.(*federationv1beta1.Cluster)
cc.knownClusterSet.Insert(cluster.Name)
// create the restclient of cluster
restClient, err := NewClusterClientSet(cluster)
if err != nil || restClient == nil {
glog.Errorf("Failed to create corresponding restclient of kubernetes cluster: %v", err)
return
}
cc.clusterKubeClientMap[cluster.Name] = *restClient
}
// Run begins watching and syncing.
func (cc *ClusterController) Run() {
defer utilruntime.HandleCrash()
go cc.clusterController.Run(wait.NeverStop)
// monitor cluster status periodically, in phase 1 we just get the health state from "/healthz"
go wait.Until(func() {
if err := cc.UpdateClusterStatus(); err != nil {
glog.Errorf("Error monitoring cluster status: %v", err)
}
}, cc.clusterMonitorPeriod, wait.NeverStop)
}
func (cc *ClusterController) GetClusterStatus(cluster *federationv1beta1.Cluster) (*federationv1beta1.ClusterStatus, error) {
// just get the status of cluster, by requesting the restapi "/healthz"
clusterClient, found := cc.clusterKubeClientMap[cluster.Name]
if !found {
glog.Infof("It's a new cluster, a cluster client will be created")
client, err := NewClusterClientSet(cluster)
if err != nil || client == nil {
glog.Errorf("Failed to create cluster client, err: %v", err)
return nil, err
}
clusterClient = *client
cc.clusterKubeClientMap[cluster.Name] = clusterClient
}
clusterStatus := clusterClient.GetClusterHealthStatus()
return clusterStatus, nil
}
// UpdateClusterStatus checks cluster status and get the metrics from cluster's restapi
func (cc *ClusterController) UpdateClusterStatus() error {
clusters, err := cc.federationClient.Federation().Clusters().List(v1.ListOptions{})
if err != nil {
return err
}
for _, cluster := range clusters.Items {
if !cc.knownClusterSet.Has(cluster.Name) {
glog.V(1).Infof("ClusterController observed a new cluster: %#v", cluster)
cc.knownClusterSet.Insert(cluster.Name)
}
}
// If there's a difference between lengths of known clusters and observed clusters
if len(cc.knownClusterSet) != len(clusters.Items) {
observedSet := make(sets.String)
for _, cluster := range clusters.Items {
observedSet.Insert(cluster.Name)
}
deleted := cc.knownClusterSet.Difference(observedSet)
for clusterName := range deleted {
glog.V(1).Infof("ClusterController observed a Cluster deletion: %v", clusterName)
cc.knownClusterSet.Delete(clusterName)
}
}
for _, cluster := range clusters.Items {
clusterStatusNew, err := cc.GetClusterStatus(&cluster)
if err != nil {
glog.Infof("Failed to Get the status of cluster: %v", cluster.Name)
continue
}
clusterStatusOld, found := cc.clusterClusterStatusMap[cluster.Name]
if !found {
glog.Infof("There is no status stored for cluster: %v before", cluster.Name)
} else {
hasTransition := false
for i := 0; i < len(clusterStatusNew.Conditions); i++ {
if !(strings.EqualFold(string(clusterStatusNew.Conditions[i].Type), string(clusterStatusOld.Conditions[i].Type)) &&
strings.EqualFold(string(clusterStatusNew.Conditions[i].Status), string(clusterStatusOld.Conditions[i].Status))) {
hasTransition = true
break
}
}
if !hasTransition {
for j := 0; j < len(clusterStatusNew.Conditions); j++ {
clusterStatusNew.Conditions[j].LastTransitionTime = clusterStatusOld.Conditions[j].LastTransitionTime
}
}
}
clusterClient, found := cc.clusterKubeClientMap[cluster.Name]
if !found {
glog.Warningf("Failed to client for cluster %s", cluster.Name)
continue
}
zones, region, err := clusterClient.GetClusterZones()
if err != nil {
glog.Warningf("Failed to get zones and region for cluster %s: %v", cluster.Name, err)
// Don't return err here, as we want the rest of the status update to proceed.
} else {
clusterStatusNew.Zones = zones
clusterStatusNew.Region = region
}
cc.clusterClusterStatusMap[cluster.Name] = *clusterStatusNew
cluster.Status = *clusterStatusNew
cluster, err := cc.federationClient.Federation().Clusters().UpdateStatus(&cluster)
if err != nil {
glog.Warningf("Failed to update the status of cluster: %v ,error is : %v", cluster.Name, err)
// Don't return err here, as we want to continue processing remaining clusters.
continue
}
}
return nil
}

View file

@ -0,0 +1,151 @@
/*
Copyright 2016 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 cluster
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
federationv1beta1 "k8s.io/kubernetes/federation/apis/federation/v1beta1"
federationclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5"
controllerutil "k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/api/v1"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
"k8s.io/kubernetes/pkg/util/uuid"
)
func newCluster(clusterName string, serverUrl string) *federationv1beta1.Cluster {
cluster := federationv1beta1.Cluster{
TypeMeta: metav1.TypeMeta{APIVersion: testapi.Federation.GroupVersion().String()},
ObjectMeta: v1.ObjectMeta{
UID: uuid.NewUUID(),
Name: clusterName,
},
Spec: federationv1beta1.ClusterSpec{
ServerAddressByClientCIDRs: []federationv1beta1.ServerAddressByClientCIDR{
{
ClientCIDR: "0.0.0.0/0",
ServerAddress: serverUrl,
},
},
},
}
return &cluster
}
func newClusterList(cluster *federationv1beta1.Cluster) *federationv1beta1.ClusterList {
clusterList := federationv1beta1.ClusterList{
TypeMeta: metav1.TypeMeta{APIVersion: testapi.Federation.GroupVersion().String()},
ListMeta: metav1.ListMeta{
SelfLink: "foobar",
},
Items: []federationv1beta1.Cluster{},
}
clusterList.Items = append(clusterList.Items, *cluster)
return &clusterList
}
// init a fake http handler, simulate a federation apiserver, response the "DELETE" "PUT" "GET" "UPDATE"
// when "canBeGotten" is false, means that user can not get the cluster cluster from apiserver
func createHttptestFakeHandlerForFederation(clusterList *federationv1beta1.ClusterList, canBeGotten bool) *http.HandlerFunc {
fakeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
clusterListString, _ := json.Marshal(*clusterList)
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case "PUT":
fmt.Fprintln(w, string(clusterListString))
case "GET":
if canBeGotten {
fmt.Fprintln(w, string(clusterListString))
} else {
fmt.Fprintln(w, "")
}
default:
fmt.Fprintln(w, "")
}
})
return &fakeHandler
}
// init a fake http handler, simulate a cluster apiserver, response the "/healthz"
// when "canBeGotten" is false, means that user can not get response from apiserver
func createHttptestFakeHandlerForCluster(canBeGotten bool) *http.HandlerFunc {
fakeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case "GET":
if canBeGotten {
fmt.Fprintln(w, "ok")
} else {
w.WriteHeader(http.StatusNotFound)
}
default:
fmt.Fprintln(w, "")
}
})
return &fakeHandler
}
func TestUpdateClusterStatusOK(t *testing.T) {
clusterName := "foobarCluster"
// create dummy httpserver
testClusterServer := httptest.NewServer(createHttptestFakeHandlerForCluster(true))
defer testClusterServer.Close()
federationCluster := newCluster(clusterName, testClusterServer.URL)
federationClusterList := newClusterList(federationCluster)
testFederationServer := httptest.NewServer(createHttptestFakeHandlerForFederation(federationClusterList, true))
defer testFederationServer.Close()
restClientCfg, err := clientcmd.BuildConfigFromFlags(testFederationServer.URL, "")
if err != nil {
t.Errorf("Failed to build client config")
}
federationClientSet := federationclientset.NewForConfigOrDie(restclient.AddUserAgent(restClientCfg, "cluster-controller"))
// Override KubeconfigGetterForCluster to avoid having to setup service accounts and mount files with secret tokens.
originalGetter := controllerutil.KubeconfigGetterForCluster
controllerutil.KubeconfigGetterForCluster = func(c *federationv1beta1.Cluster) clientcmd.KubeconfigGetter {
return func() (*clientcmdapi.Config, error) {
return &clientcmdapi.Config{}, nil
}
}
manager := NewclusterController(federationClientSet, 5)
err = manager.UpdateClusterStatus()
if err != nil {
t.Errorf("Failed to Update Cluster Status: %v", err)
}
clusterStatus, found := manager.clusterClusterStatusMap[clusterName]
if !found {
t.Errorf("Failed to Update Cluster Status")
} else {
if (clusterStatus.Conditions[1].Status != v1.ConditionFalse) || (clusterStatus.Conditions[1].Type != federationv1beta1.ClusterOffline) {
t.Errorf("Failed to Update Cluster Status")
}
}
// Reset KubeconfigGetterForCluster
controllerutil.KubeconfigGetterForCluster = originalGetter
}

View file

@ -0,0 +1,18 @@
/*
Copyright 2016 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 cluster contains code for syncing cluster
package cluster // import "k8s.io/kubernetes/federation/pkg/federation-controller/cluster"

View file

@ -0,0 +1,54 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["configmap_controller.go"],
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/eventsink:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/record:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/types:go_default_library",
"//pkg/util/flowcontrol:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = ["configmap_controller_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5/fake:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/test:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/clientset_generated/release_1_5/fake:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/types:go_default_library",
"//pkg/util/wait:go_default_library",
"//vendor:github.com/stretchr/testify/assert",
],
)

View file

@ -0,0 +1,315 @@
/*
Copyright 2016 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 configmap
import (
"time"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
federationclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/federation/pkg/federation-controller/util/eventsink"
"k8s.io/kubernetes/pkg/api"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/client/cache"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
"k8s.io/kubernetes/pkg/client/record"
"k8s.io/kubernetes/pkg/controller"
pkgruntime "k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/flowcontrol"
"k8s.io/kubernetes/pkg/watch"
"github.com/golang/glog"
)
const (
allClustersKey = "ALL_CLUSTERS"
)
type ConfigMapController struct {
// For triggering single configmap reconciliation. This is used when there is an
// add/update/delete operation on a configmap in either federated API server or
// in some member of the federation.
configmapDeliverer *util.DelayingDeliverer
// For triggering all configmaps reconciliation. This is used when
// a new cluster becomes available.
clusterDeliverer *util.DelayingDeliverer
// Contains configmaps present in members of federation.
configmapFederatedInformer util.FederatedInformer
// For updating members of federation.
federatedUpdater util.FederatedUpdater
// Definitions of configmaps that should be federated.
configmapInformerStore cache.Store
// Informer controller for configmaps that should be federated.
configmapInformerController cache.ControllerInterface
// Client to federated api server.
federatedApiClient federationclientset.Interface
// Backoff manager for configmaps
configmapBackoff *flowcontrol.Backoff
// For events
eventRecorder record.EventRecorder
configmapReviewDelay time.Duration
clusterAvailableDelay time.Duration
smallDelay time.Duration
updateTimeout time.Duration
}
// NewConfigMapController returns a new configmap controller
func NewConfigMapController(client federationclientset.Interface) *ConfigMapController {
broadcaster := record.NewBroadcaster()
broadcaster.StartRecordingToSink(eventsink.NewFederatedEventSink(client))
recorder := broadcaster.NewRecorder(apiv1.EventSource{Component: "federated-configmaps-controller"})
configmapcontroller := &ConfigMapController{
federatedApiClient: client,
configmapReviewDelay: time.Second * 10,
clusterAvailableDelay: time.Second * 20,
smallDelay: time.Second * 3,
updateTimeout: time.Second * 30,
configmapBackoff: flowcontrol.NewBackOff(5*time.Second, time.Minute),
eventRecorder: recorder,
}
// Build delivereres for triggering reconciliations.
configmapcontroller.configmapDeliverer = util.NewDelayingDeliverer()
configmapcontroller.clusterDeliverer = util.NewDelayingDeliverer()
// Start informer on federated API servers on configmaps that should be federated.
configmapcontroller.configmapInformerStore, configmapcontroller.configmapInformerController = cache.NewInformer(
&cache.ListWatch{
ListFunc: func(options apiv1.ListOptions) (pkgruntime.Object, error) {
return client.Core().ConfigMaps(apiv1.NamespaceAll).List(options)
},
WatchFunc: func(options apiv1.ListOptions) (watch.Interface, error) {
return client.Core().ConfigMaps(apiv1.NamespaceAll).Watch(options)
},
},
&apiv1.ConfigMap{},
controller.NoResyncPeriodFunc(),
util.NewTriggerOnAllChanges(func(obj pkgruntime.Object) { configmapcontroller.deliverConfigMapObj(obj, 0, false) }))
// Federated informer on configmaps in members of federation.
configmapcontroller.configmapFederatedInformer = util.NewFederatedInformer(
client,
func(cluster *federationapi.Cluster, targetClient kubeclientset.Interface) (cache.Store, cache.ControllerInterface) {
return cache.NewInformer(
&cache.ListWatch{
ListFunc: func(options apiv1.ListOptions) (pkgruntime.Object, error) {
return targetClient.Core().ConfigMaps(apiv1.NamespaceAll).List(options)
},
WatchFunc: func(options apiv1.ListOptions) (watch.Interface, error) {
return targetClient.Core().ConfigMaps(apiv1.NamespaceAll).Watch(options)
},
},
&apiv1.ConfigMap{},
controller.NoResyncPeriodFunc(),
// Trigger reconciliation whenever something in federated cluster is changed. In most cases it
// would be just confirmation that some configmap opration succeeded.
util.NewTriggerOnAllChanges(
func(obj pkgruntime.Object) {
configmapcontroller.deliverConfigMapObj(obj, configmapcontroller.configmapReviewDelay, false)
},
))
},
&util.ClusterLifecycleHandlerFuncs{
ClusterAvailable: func(cluster *federationapi.Cluster) {
// When new cluster becomes available process all the configmaps again.
configmapcontroller.clusterDeliverer.DeliverAt(allClustersKey, nil, time.Now().Add(configmapcontroller.clusterAvailableDelay))
},
},
)
// Federated updater along with Create/Update/Delete operations.
configmapcontroller.federatedUpdater = util.NewFederatedUpdater(configmapcontroller.configmapFederatedInformer,
func(client kubeclientset.Interface, obj pkgruntime.Object) error {
configmap := obj.(*apiv1.ConfigMap)
_, err := client.Core().ConfigMaps(configmap.Namespace).Create(configmap)
return err
},
func(client kubeclientset.Interface, obj pkgruntime.Object) error {
configmap := obj.(*apiv1.ConfigMap)
_, err := client.Core().ConfigMaps(configmap.Namespace).Update(configmap)
return err
},
func(client kubeclientset.Interface, obj pkgruntime.Object) error {
configmap := obj.(*apiv1.ConfigMap)
err := client.Core().ConfigMaps(configmap.Namespace).Delete(configmap.Name, &apiv1.DeleteOptions{})
return err
})
return configmapcontroller
}
func (configmapcontroller *ConfigMapController) Run(stopChan <-chan struct{}) {
go configmapcontroller.configmapInformerController.Run(stopChan)
configmapcontroller.configmapFederatedInformer.Start()
go func() {
<-stopChan
configmapcontroller.configmapFederatedInformer.Stop()
}()
configmapcontroller.configmapDeliverer.StartWithHandler(func(item *util.DelayingDelivererItem) {
configmap := item.Value.(*types.NamespacedName)
configmapcontroller.reconcileConfigMap(*configmap)
})
configmapcontroller.clusterDeliverer.StartWithHandler(func(_ *util.DelayingDelivererItem) {
configmapcontroller.reconcileConfigMapsOnClusterChange()
})
util.StartBackoffGC(configmapcontroller.configmapBackoff, stopChan)
}
func (configmapcontroller *ConfigMapController) deliverConfigMapObj(obj interface{}, delay time.Duration, failed bool) {
configmap := obj.(*apiv1.ConfigMap)
configmapcontroller.deliverConfigMap(types.NamespacedName{Namespace: configmap.Namespace, Name: configmap.Name}, delay, failed)
}
// Adds backoff to delay if this delivery is related to some failure. Resets backoff if there was no failure.
func (configmapcontroller *ConfigMapController) deliverConfigMap(configmap types.NamespacedName, delay time.Duration, failed bool) {
key := configmap.String()
if failed {
configmapcontroller.configmapBackoff.Next(key, time.Now())
delay = delay + configmapcontroller.configmapBackoff.Get(key)
} else {
configmapcontroller.configmapBackoff.Reset(key)
}
configmapcontroller.configmapDeliverer.DeliverAfter(key, &configmap, delay)
}
// Check whether all data stores are in sync. False is returned if any of the informer/stores is not yet
// synced with the corresponding api server.
func (configmapcontroller *ConfigMapController) isSynced() bool {
if !configmapcontroller.configmapFederatedInformer.ClustersSynced() {
glog.V(2).Infof("Cluster list not synced")
return false
}
clusters, err := configmapcontroller.configmapFederatedInformer.GetReadyClusters()
if err != nil {
glog.Errorf("Failed to get ready clusters: %v", err)
return false
}
if !configmapcontroller.configmapFederatedInformer.GetTargetStore().ClustersSynced(clusters) {
return false
}
return true
}
// The function triggers reconciliation of all federated configmaps.
func (configmapcontroller *ConfigMapController) reconcileConfigMapsOnClusterChange() {
if !configmapcontroller.isSynced() {
glog.V(4).Infof("Configmap controller not synced")
configmapcontroller.clusterDeliverer.DeliverAt(allClustersKey, nil, time.Now().Add(configmapcontroller.clusterAvailableDelay))
}
for _, obj := range configmapcontroller.configmapInformerStore.List() {
configmap := obj.(*apiv1.ConfigMap)
configmapcontroller.deliverConfigMap(types.NamespacedName{Namespace: configmap.Namespace, Name: configmap.Name},
configmapcontroller.smallDelay, false)
}
}
func (configmapcontroller *ConfigMapController) reconcileConfigMap(configmap types.NamespacedName) {
if !configmapcontroller.isSynced() {
glog.V(4).Infof("Configmap controller not synced")
configmapcontroller.deliverConfigMap(configmap, configmapcontroller.clusterAvailableDelay, false)
return
}
key := configmap.String()
baseConfigMapObj, exist, err := configmapcontroller.configmapInformerStore.GetByKey(key)
if err != nil {
glog.Errorf("Failed to query main configmap store for %v: %v", key, err)
configmapcontroller.deliverConfigMap(configmap, 0, true)
return
}
if !exist {
// Not federated configmap, ignoring.
glog.V(8).Infof("Skipping not federated config map: %s", key)
return
}
baseConfigMap := baseConfigMapObj.(*apiv1.ConfigMap)
clusters, err := configmapcontroller.configmapFederatedInformer.GetReadyClusters()
if err != nil {
glog.Errorf("Failed to get cluster list: %v, retrying shortly", err)
configmapcontroller.deliverConfigMap(configmap, configmapcontroller.clusterAvailableDelay, false)
return
}
operations := make([]util.FederatedOperation, 0)
for _, cluster := range clusters {
clusterConfigMapObj, found, err := configmapcontroller.configmapFederatedInformer.GetTargetStore().GetByKey(cluster.Name, key)
if err != nil {
glog.Errorf("Failed to get %s from %s: %v, retrying shortly", key, cluster.Name, err)
configmapcontroller.deliverConfigMap(configmap, 0, true)
return
}
// Do not modify data.
desiredConfigMap := &apiv1.ConfigMap{
ObjectMeta: util.DeepCopyRelevantObjectMeta(baseConfigMap.ObjectMeta),
Data: baseConfigMap.Data,
}
if !found {
configmapcontroller.eventRecorder.Eventf(baseConfigMap, api.EventTypeNormal, "CreateInCluster",
"Creating configmap in cluster %s", cluster.Name)
operations = append(operations, util.FederatedOperation{
Type: util.OperationTypeAdd,
Obj: desiredConfigMap,
ClusterName: cluster.Name,
})
} else {
clusterConfigMap := clusterConfigMapObj.(*apiv1.ConfigMap)
// Update existing configmap, if needed.
if !util.ConfigMapEquivalent(desiredConfigMap, clusterConfigMap) {
configmapcontroller.eventRecorder.Eventf(baseConfigMap, api.EventTypeNormal, "UpdateInCluster",
"Updating configmap in cluster %s", cluster.Name)
operations = append(operations, util.FederatedOperation{
Type: util.OperationTypeUpdate,
Obj: desiredConfigMap,
ClusterName: cluster.Name,
})
}
}
}
if len(operations) == 0 {
// Everything is in order
glog.V(8).Infof("No operations needed for %s", key)
return
}
err = configmapcontroller.federatedUpdater.UpdateWithOnError(operations, configmapcontroller.updateTimeout,
func(op util.FederatedOperation, operror error) {
configmapcontroller.eventRecorder.Eventf(baseConfigMap, api.EventTypeNormal, "UpdateInClusterFailed",
"ConfigMap update in cluster %s failed: %v", op.ClusterName, operror)
})
if err != nil {
glog.Errorf("Failed to execute updates for %s: %v, retrying shortly", key, err)
configmapcontroller.deliverConfigMap(configmap, 0, true)
return
}
}

View file

@ -0,0 +1,142 @@
/*
Copyright 2016 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 configmap
import (
"fmt"
"testing"
"time"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
fakefedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5/fake"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
. "k8s.io/kubernetes/federation/pkg/federation-controller/util/test"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
fakekubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5/fake"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/wait"
"github.com/stretchr/testify/assert"
)
func TestConfigMapController(t *testing.T) {
cluster1 := NewCluster("cluster1", apiv1.ConditionTrue)
cluster2 := NewCluster("cluster2", apiv1.ConditionTrue)
fakeClient := &fakefedclientset.Clientset{}
RegisterFakeList("clusters", &fakeClient.Fake, &federationapi.ClusterList{Items: []federationapi.Cluster{*cluster1}})
RegisterFakeList("configmaps", &fakeClient.Fake, &apiv1.ConfigMapList{Items: []apiv1.ConfigMap{}})
configmapWatch := RegisterFakeWatch("configmaps", &fakeClient.Fake)
clusterWatch := RegisterFakeWatch("clusters", &fakeClient.Fake)
cluster1Client := &fakekubeclientset.Clientset{}
cluster1Watch := RegisterFakeWatch("configmaps", &cluster1Client.Fake)
RegisterFakeList("configmaps", &cluster1Client.Fake, &apiv1.ConfigMapList{Items: []apiv1.ConfigMap{}})
cluster1CreateChan := RegisterFakeCopyOnCreate("configmaps", &cluster1Client.Fake, cluster1Watch)
cluster1UpdateChan := RegisterFakeCopyOnUpdate("configmaps", &cluster1Client.Fake, cluster1Watch)
cluster2Client := &fakekubeclientset.Clientset{}
cluster2Watch := RegisterFakeWatch("configmaps", &cluster2Client.Fake)
RegisterFakeList("configmaps", &cluster2Client.Fake, &apiv1.ConfigMapList{Items: []apiv1.ConfigMap{}})
cluster2CreateChan := RegisterFakeCopyOnCreate("configmaps", &cluster2Client.Fake, cluster2Watch)
configmapController := NewConfigMapController(fakeClient)
informer := ToFederatedInformerForTestOnly(configmapController.configmapFederatedInformer)
informer.SetClientFactory(func(cluster *federationapi.Cluster) (kubeclientset.Interface, error) {
switch cluster.Name {
case cluster1.Name:
return cluster1Client, nil
case cluster2.Name:
return cluster2Client, nil
default:
return nil, fmt.Errorf("Unknown cluster")
}
})
configmapController.clusterAvailableDelay = time.Second
configmapController.configmapReviewDelay = 50 * time.Millisecond
configmapController.smallDelay = 20 * time.Millisecond
configmapController.updateTimeout = 5 * time.Second
stop := make(chan struct{})
configmapController.Run(stop)
configmap1 := &apiv1.ConfigMap{
ObjectMeta: apiv1.ObjectMeta{
Name: "test-configmap",
Namespace: "ns",
SelfLink: "/api/v1/namespaces/ns/configmaps/test-configmap",
},
Data: map[string]string{
"A": "ala ma kota",
"B": "quick brown fox",
},
}
// Test add federated configmap.
configmapWatch.Add(configmap1)
createdConfigMap := GetConfigMapFromChan(cluster1CreateChan)
assert.NotNil(t, createdConfigMap)
assert.Equal(t, configmap1.Namespace, createdConfigMap.Namespace)
assert.Equal(t, configmap1.Name, createdConfigMap.Name)
assert.True(t, util.ConfigMapEquivalent(configmap1, createdConfigMap))
// Wait for the configmap to appear in the informer store
err := WaitForStoreUpdate(
configmapController.configmapFederatedInformer.GetTargetStore(),
cluster1.Name, types.NamespacedName{Namespace: configmap1.Namespace, Name: configmap1.Name}.String(), wait.ForeverTestTimeout)
assert.Nil(t, err, "configmap should have appeared in the informer store")
// Test update federated configmap.
configmap1.Annotations = map[string]string{
"A": "B",
}
configmapWatch.Modify(configmap1)
updatedConfigMap := GetConfigMapFromChan(cluster1UpdateChan)
assert.NotNil(t, updatedConfigMap)
assert.Equal(t, configmap1.Name, updatedConfigMap.Name)
assert.Equal(t, configmap1.Namespace, updatedConfigMap.Namespace)
assert.True(t, util.ConfigMapEquivalent(configmap1, updatedConfigMap))
// Test update federated configmap.
configmap1.Data = map[string]string{
"config": "myconfigurationfile",
}
configmapWatch.Modify(configmap1)
updatedConfigMap2 := GetConfigMapFromChan(cluster1UpdateChan)
assert.NotNil(t, updatedConfigMap)
assert.Equal(t, configmap1.Name, updatedConfigMap.Name)
assert.Equal(t, configmap1.Namespace, updatedConfigMap.Namespace)
assert.True(t, util.ConfigMapEquivalent(configmap1, updatedConfigMap2))
// Test add cluster
clusterWatch.Add(cluster2)
createdConfigMap2 := GetConfigMapFromChan(cluster2CreateChan)
assert.NotNil(t, createdConfigMap2)
assert.Equal(t, configmap1.Name, createdConfigMap2.Name)
assert.Equal(t, configmap1.Namespace, createdConfigMap2.Namespace)
assert.True(t, util.ConfigMapEquivalent(configmap1, createdConfigMap2))
close(stop)
}
func GetConfigMapFromChan(c chan runtime.Object) *apiv1.ConfigMap {
configmap := GetObjectFromChan(c).(*apiv1.ConfigMap)
return configmap
}

View file

@ -0,0 +1,60 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["daemonset_controller.go"],
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/deletionhelper:go_default_library",
"//federation/pkg/federation-controller/util/eventsink:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/errors:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/record:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/conversion:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/types:go_default_library",
"//pkg/util/flowcontrol:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = ["daemonset_controller_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5/fake:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/deletionhelper:go_default_library",
"//federation/pkg/federation-controller/util/test:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/clientset_generated/release_1_5/fake:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/util/wait:go_default_library",
"//vendor:github.com/stretchr/testify/assert",
],
)

View file

@ -0,0 +1,472 @@
/*
Copyright 2016 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 daemonset
import (
"fmt"
"reflect"
"time"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
federationclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/federation/pkg/federation-controller/util/deletionhelper"
"k8s.io/kubernetes/federation/pkg/federation-controller/util/eventsink"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
extensionsv1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
"k8s.io/kubernetes/pkg/client/cache"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
"k8s.io/kubernetes/pkg/client/record"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/conversion"
pkgruntime "k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/flowcontrol"
"k8s.io/kubernetes/pkg/watch"
"github.com/golang/glog"
)
const (
allClustersKey = "ALL_CLUSTERS"
)
type DaemonSetController struct {
// For triggering single daemonset reconciliation. This is used when there is an
// add/update/delete operation on a daemonset in either federated API server or
// in some member of the federation.
daemonsetDeliverer *util.DelayingDeliverer
// For triggering all daemonsets reconciliation. This is used when
// a new cluster becomes available.
clusterDeliverer *util.DelayingDeliverer
// Contains daemonsets present in members of federation.
daemonsetFederatedInformer util.FederatedInformer
// For updating members of federation.
federatedUpdater util.FederatedUpdater
// Definitions of daemonsets that should be federated.
daemonsetInformerStore cache.Store
// Informer controller for daemonsets that should be federated.
daemonsetInformerController cache.ControllerInterface
// Client to federated api server.
federatedApiClient federationclientset.Interface
// Backoff manager for daemonsets
daemonsetBackoff *flowcontrol.Backoff
// For events
eventRecorder record.EventRecorder
deletionHelper *deletionhelper.DeletionHelper
daemonsetReviewDelay time.Duration
clusterAvailableDelay time.Duration
smallDelay time.Duration
updateTimeout time.Duration
}
// NewDaemonSetController returns a new daemonset controller
func NewDaemonSetController(client federationclientset.Interface) *DaemonSetController {
broadcaster := record.NewBroadcaster()
broadcaster.StartRecordingToSink(eventsink.NewFederatedEventSink(client))
recorder := broadcaster.NewRecorder(apiv1.EventSource{Component: "federated-daemonset-controller"})
daemonsetcontroller := &DaemonSetController{
federatedApiClient: client,
daemonsetReviewDelay: time.Second * 10,
clusterAvailableDelay: time.Second * 20,
smallDelay: time.Second * 3,
updateTimeout: time.Second * 30,
daemonsetBackoff: flowcontrol.NewBackOff(5*time.Second, time.Minute),
eventRecorder: recorder,
}
// Build deliverers for triggering reconciliations.
daemonsetcontroller.daemonsetDeliverer = util.NewDelayingDeliverer()
daemonsetcontroller.clusterDeliverer = util.NewDelayingDeliverer()
// Start informer in federated API servers on daemonsets that should be federated.
daemonsetcontroller.daemonsetInformerStore, daemonsetcontroller.daemonsetInformerController = cache.NewInformer(
&cache.ListWatch{
ListFunc: func(options apiv1.ListOptions) (pkgruntime.Object, error) {
return client.Extensions().DaemonSets(apiv1.NamespaceAll).List(options)
},
WatchFunc: func(options apiv1.ListOptions) (watch.Interface, error) {
return client.Extensions().DaemonSets(apiv1.NamespaceAll).Watch(options)
},
},
&extensionsv1.DaemonSet{},
controller.NoResyncPeriodFunc(),
util.NewTriggerOnAllChanges(func(obj pkgruntime.Object) { daemonsetcontroller.deliverDaemonSetObj(obj, 0, false) }))
// Federated informer on daemonsets in members of federation.
daemonsetcontroller.daemonsetFederatedInformer = util.NewFederatedInformer(
client,
func(cluster *federationapi.Cluster, targetClient kubeclientset.Interface) (cache.Store, cache.ControllerInterface) {
return cache.NewInformer(
&cache.ListWatch{
ListFunc: func(options apiv1.ListOptions) (pkgruntime.Object, error) {
return targetClient.Extensions().DaemonSets(apiv1.NamespaceAll).List(options)
},
WatchFunc: func(options apiv1.ListOptions) (watch.Interface, error) {
return targetClient.Extensions().DaemonSets(apiv1.NamespaceAll).Watch(options)
},
},
&extensionsv1.DaemonSet{},
controller.NoResyncPeriodFunc(),
// Trigger reconciliation whenever something in federated cluster is changed. In most cases it
// would be just confirmation that some daemonset opration succeeded.
util.NewTriggerOnAllChanges(
func(obj pkgruntime.Object) {
daemonsetcontroller.deliverDaemonSetObj(obj, daemonsetcontroller.daemonsetReviewDelay, false)
},
))
},
&util.ClusterLifecycleHandlerFuncs{
ClusterAvailable: func(cluster *federationapi.Cluster) {
// When new cluster becomes available process all the daemonsets again.
daemonsetcontroller.clusterDeliverer.DeliverAt(allClustersKey, nil, time.Now().Add(daemonsetcontroller.clusterAvailableDelay))
},
},
)
// Federated updater along with Create/Update/Delete operations.
daemonsetcontroller.federatedUpdater = util.NewFederatedUpdater(daemonsetcontroller.daemonsetFederatedInformer,
func(client kubeclientset.Interface, obj pkgruntime.Object) error {
daemonset := obj.(*extensionsv1.DaemonSet)
glog.V(4).Infof("Attempting to create daemonset: %s/%s", daemonset.Namespace, daemonset.Name)
_, err := client.Extensions().DaemonSets(daemonset.Namespace).Create(daemonset)
if err != nil {
glog.Errorf("Error creating daemonset %s/%s/: %v", daemonset.Namespace, daemonset.Name, err)
} else {
glog.V(4).Infof("Successfully created deamonset %s/%s", daemonset.Namespace, daemonset.Name)
}
return err
},
func(client kubeclientset.Interface, obj pkgruntime.Object) error {
daemonset := obj.(*extensionsv1.DaemonSet)
glog.V(4).Infof("Attempting to update daemonset: %s/%s", daemonset.Namespace, daemonset.Name)
_, err := client.Extensions().DaemonSets(daemonset.Namespace).Update(daemonset)
if err != nil {
glog.Errorf("Error updating daemonset %s/%s/: %v", daemonset.Namespace, daemonset.Name, err)
} else {
glog.V(4).Infof("Successfully updating deamonset %s/%s", daemonset.Namespace, daemonset.Name)
}
return err
},
func(client kubeclientset.Interface, obj pkgruntime.Object) error {
daemonset := obj.(*extensionsv1.DaemonSet)
glog.V(4).Infof("Attempting to delete daemonset: %s/%s", daemonset.Namespace, daemonset.Name)
err := client.Extensions().DaemonSets(daemonset.Namespace).Delete(daemonset.Name, &apiv1.DeleteOptions{})
if err != nil {
glog.Errorf("Error deleting daemonset %s/%s/: %v", daemonset.Namespace, daemonset.Name, err)
} else {
glog.V(4).Infof("Successfully deleting deamonset %s/%s", daemonset.Namespace, daemonset.Name)
}
return err
})
daemonsetcontroller.deletionHelper = deletionhelper.NewDeletionHelper(
daemonsetcontroller.hasFinalizerFunc,
daemonsetcontroller.removeFinalizerFunc,
daemonsetcontroller.addFinalizerFunc,
// objNameFunc
func(obj pkgruntime.Object) string {
daemonset := obj.(*extensionsv1.DaemonSet)
return daemonset.Name
},
daemonsetcontroller.updateTimeout,
daemonsetcontroller.eventRecorder,
daemonsetcontroller.daemonsetFederatedInformer,
daemonsetcontroller.federatedUpdater,
)
return daemonsetcontroller
}
// Returns true if the given object has the given finalizer in its ObjectMeta.
func (daemonsetcontroller *DaemonSetController) hasFinalizerFunc(obj pkgruntime.Object, finalizer string) bool {
daemonset := obj.(*extensionsv1.DaemonSet)
for i := range daemonset.ObjectMeta.Finalizers {
if string(daemonset.ObjectMeta.Finalizers[i]) == finalizer {
return true
}
}
return false
}
// Removes the finalizer from the given objects ObjectMeta.
// Assumes that the given object is a daemonset.
func (daemonsetcontroller *DaemonSetController) removeFinalizerFunc(obj pkgruntime.Object, finalizer string) (pkgruntime.Object, error) {
daemonset := obj.(*extensionsv1.DaemonSet)
newFinalizers := []string{}
hasFinalizer := false
for i := range daemonset.ObjectMeta.Finalizers {
if string(daemonset.ObjectMeta.Finalizers[i]) != finalizer {
newFinalizers = append(newFinalizers, daemonset.ObjectMeta.Finalizers[i])
} else {
hasFinalizer = true
}
}
if !hasFinalizer {
// Nothing to do.
return obj, nil
}
daemonset.ObjectMeta.Finalizers = newFinalizers
daemonset, err := daemonsetcontroller.federatedApiClient.Extensions().DaemonSets(daemonset.Namespace).Update(daemonset)
if err != nil {
return nil, fmt.Errorf("failed to remove finalizer %s from daemonset %s: %v", finalizer, daemonset.Name, err)
}
return daemonset, nil
}
// Adds the given finalizer to the given objects ObjectMeta.
// Assumes that the given object is a daemonset.
func (daemonsetcontroller *DaemonSetController) addFinalizerFunc(obj pkgruntime.Object, finalizer string) (pkgruntime.Object, error) {
daemonset := obj.(*extensionsv1.DaemonSet)
daemonset.ObjectMeta.Finalizers = append(daemonset.ObjectMeta.Finalizers, finalizer)
daemonset, err := daemonsetcontroller.federatedApiClient.Extensions().DaemonSets(daemonset.Namespace).Update(daemonset)
if err != nil {
return nil, fmt.Errorf("failed to add finalizer %s to daemonset %s: %v", finalizer, daemonset.Name, err)
}
return daemonset, nil
}
func (daemonsetcontroller *DaemonSetController) Run(stopChan <-chan struct{}) {
glog.V(1).Infof("Starting daemonset controllr")
go daemonsetcontroller.daemonsetInformerController.Run(stopChan)
glog.V(1).Infof("Starting daemonset federated informer")
daemonsetcontroller.daemonsetFederatedInformer.Start()
go func() {
<-stopChan
daemonsetcontroller.daemonsetFederatedInformer.Stop()
}()
glog.V(1).Infof("Starting daemonset deliverers")
daemonsetcontroller.daemonsetDeliverer.StartWithHandler(func(item *util.DelayingDelivererItem) {
daemonset := item.Value.(*types.NamespacedName)
glog.V(4).Infof("Trigerring reconciliation of daemonset %s", daemonset.String())
daemonsetcontroller.reconcileDaemonSet(daemonset.Namespace, daemonset.Name)
})
daemonsetcontroller.clusterDeliverer.StartWithHandler(func(_ *util.DelayingDelivererItem) {
glog.V(4).Infof("Triggering reconciliation of all daemonsets")
daemonsetcontroller.reconcileDaemonSetsOnClusterChange()
})
util.StartBackoffGC(daemonsetcontroller.daemonsetBackoff, stopChan)
}
func getDaemonSetKey(namespace, name string) string {
return types.NamespacedName{
Namespace: namespace,
Name: name,
}.String()
}
func (daemonsetcontroller *DaemonSetController) deliverDaemonSetObj(obj interface{}, delay time.Duration, failed bool) {
daemonset := obj.(*extensionsv1.DaemonSet)
daemonsetcontroller.deliverDaemonSet(daemonset.Namespace, daemonset.Name, delay, failed)
}
// Adds backoff to delay if this delivery is related to some failure. Resets backoff if there was no failure.
func (daemonsetcontroller *DaemonSetController) deliverDaemonSet(namespace string, name string, delay time.Duration, failed bool) {
key := getDaemonSetKey(namespace, name)
if failed {
daemonsetcontroller.daemonsetBackoff.Next(key, time.Now())
delay = delay + daemonsetcontroller.daemonsetBackoff.Get(key)
} else {
daemonsetcontroller.daemonsetBackoff.Reset(key)
}
daemonsetcontroller.daemonsetDeliverer.DeliverAfter(key,
&types.NamespacedName{Namespace: namespace, Name: name}, delay)
}
// Check whether all data stores are in sync. False is returned if any of the informer/stores is not yet
// synced with the corresponding api server.
func (daemonsetcontroller *DaemonSetController) isSynced() bool {
if !daemonsetcontroller.daemonsetFederatedInformer.ClustersSynced() {
glog.V(2).Infof("Cluster list not synced")
return false
}
clusters, err := daemonsetcontroller.daemonsetFederatedInformer.GetReadyClusters()
if err != nil {
glog.Errorf("Failed to get ready clusters: %v", err)
return false
}
if !daemonsetcontroller.daemonsetFederatedInformer.GetTargetStore().ClustersSynced(clusters) {
return false
}
return true
}
// The function triggers reconciliation of all federated daemonsets.
func (daemonsetcontroller *DaemonSetController) reconcileDaemonSetsOnClusterChange() {
if !daemonsetcontroller.isSynced() {
daemonsetcontroller.clusterDeliverer.DeliverAt(allClustersKey, nil, time.Now().Add(daemonsetcontroller.clusterAvailableDelay))
}
for _, obj := range daemonsetcontroller.daemonsetInformerStore.List() {
daemonset := obj.(*extensionsv1.DaemonSet)
daemonsetcontroller.deliverDaemonSet(daemonset.Namespace, daemonset.Name, daemonsetcontroller.smallDelay, false)
}
}
func (daemonsetcontroller *DaemonSetController) reconcileDaemonSet(namespace string, daemonsetName string) {
glog.V(4).Infof("Reconciling daemonset %s/%s", namespace, daemonsetName)
if !daemonsetcontroller.isSynced() {
glog.V(4).Infof("Daemonset controller is not synced")
daemonsetcontroller.deliverDaemonSet(namespace, daemonsetName, daemonsetcontroller.clusterAvailableDelay, false)
return
}
key := getDaemonSetKey(namespace, daemonsetName)
baseDaemonSetObjFromStore, exist, err := daemonsetcontroller.daemonsetInformerStore.GetByKey(key)
if err != nil {
glog.Errorf("Failed to query main daemonset store for %v: %v", key, err)
daemonsetcontroller.deliverDaemonSet(namespace, daemonsetName, 0, true)
return
}
if !exist {
glog.V(4).Infof("Skipping daemonset %s/%s - not federated", namespace, daemonsetName)
// Not federated daemonset, ignoring.
return
}
baseDaemonSetObj, err := conversion.NewCloner().DeepCopy(baseDaemonSetObjFromStore)
baseDaemonSet, ok := baseDaemonSetObj.(*extensionsv1.DaemonSet)
if err != nil || !ok {
glog.Errorf("Error in retrieving obj %s from store: %v, %v", daemonsetName, ok, err)
daemonsetcontroller.deliverDaemonSet(namespace, daemonsetName, 0, true)
return
}
if baseDaemonSet.DeletionTimestamp != nil {
if err := daemonsetcontroller.delete(baseDaemonSet); err != nil {
glog.Errorf("Failed to delete %s: %v", daemonsetName, err)
daemonsetcontroller.eventRecorder.Eventf(baseDaemonSet, api.EventTypeNormal, "DeleteFailed",
"DaemonSet delete failed: %v", err)
daemonsetcontroller.deliverDaemonSet(namespace, daemonsetName, 0, true)
}
return
}
glog.V(3).Infof("Ensuring delete object from underlying clusters finalizer for daemonset: %s",
baseDaemonSet.Name)
// Add the required finalizers before creating a daemonset in underlying clusters.
updatedDaemonSetObj, err := daemonsetcontroller.deletionHelper.EnsureFinalizers(baseDaemonSet)
if err != nil {
glog.Errorf("Failed to ensure delete object from underlying clusters finalizer in daemonset %s: %v",
baseDaemonSet.Name, err)
daemonsetcontroller.deliverDaemonSet(namespace, daemonsetName, 0, false)
return
}
baseDaemonSet = updatedDaemonSetObj.(*extensionsv1.DaemonSet)
glog.V(3).Infof("Syncing daemonset %s in underlying clusters", baseDaemonSet.Name)
clusters, err := daemonsetcontroller.daemonsetFederatedInformer.GetReadyClusters()
if err != nil {
glog.Errorf("Failed to get cluster list: %v", err)
daemonsetcontroller.deliverDaemonSet(namespace, daemonsetName, daemonsetcontroller.clusterAvailableDelay, false)
return
}
operations := make([]util.FederatedOperation, 0)
for _, cluster := range clusters {
clusterDaemonSetObj, found, err := daemonsetcontroller.daemonsetFederatedInformer.GetTargetStore().GetByKey(cluster.Name, key)
if err != nil {
glog.Errorf("Failed to get %s from %s: %v", key, cluster.Name, err)
daemonsetcontroller.deliverDaemonSet(namespace, daemonsetName, 0, true)
return
}
// Do not modify. Otherwise make a deep copy.
desiredDaemonSet := &extensionsv1.DaemonSet{
ObjectMeta: util.DeepCopyRelevantObjectMeta(baseDaemonSet.ObjectMeta),
Spec: util.DeepCopyApiTypeOrPanic(baseDaemonSet.Spec).(extensionsv1.DaemonSetSpec),
}
if !found {
glog.V(4).Infof("Creating daemonset %s/%s in cluster %s", namespace, daemonsetName, cluster.Name)
daemonsetcontroller.eventRecorder.Eventf(baseDaemonSet, api.EventTypeNormal, "CreateInCluster",
"Creating daemonset in cluster %s", cluster.Name)
operations = append(operations, util.FederatedOperation{
Type: util.OperationTypeAdd,
Obj: desiredDaemonSet,
ClusterName: cluster.Name,
})
} else {
clusterDaemonSet := clusterDaemonSetObj.(*extensionsv1.DaemonSet)
// Update existing daemonset, if needed.
if !util.ObjectMetaEquivalent(desiredDaemonSet.ObjectMeta, clusterDaemonSet.ObjectMeta) ||
!reflect.DeepEqual(desiredDaemonSet.Spec, clusterDaemonSet.Spec) {
glog.V(4).Infof("Upadting daemonset %s/%s in cluster %s", namespace, daemonsetName, cluster.Name)
daemonsetcontroller.eventRecorder.Eventf(baseDaemonSet, api.EventTypeNormal, "UpdateInCluster",
"Updating daemonset in cluster %s", cluster.Name)
operations = append(operations, util.FederatedOperation{
Type: util.OperationTypeUpdate,
Obj: desiredDaemonSet,
ClusterName: cluster.Name,
})
}
}
}
if len(operations) == 0 {
glog.V(4).Infof("No operation needed for %s/%s", namespace, daemonsetName)
// Everything is in order
return
}
err = daemonsetcontroller.federatedUpdater.UpdateWithOnError(operations, daemonsetcontroller.updateTimeout,
func(op util.FederatedOperation, operror error) {
daemonsetcontroller.eventRecorder.Eventf(baseDaemonSet, api.EventTypeNormal, "UpdateInClusterFailed",
"DaemonSet update in cluster %s failed: %v", op.ClusterName, operror)
})
if err != nil {
glog.Errorf("Failed to execute updates for %s: %v, retrying shortly", key, err)
daemonsetcontroller.deliverDaemonSet(namespace, daemonsetName, 0, true)
return
}
}
// delete deletes the given daemonset or returns error if the deletion was not complete.
func (daemonsetcontroller *DaemonSetController) delete(daemonset *extensionsv1.DaemonSet) error {
glog.V(3).Infof("Handling deletion of daemonset: %v", *daemonset)
_, err := daemonsetcontroller.deletionHelper.HandleObjectInUnderlyingClusters(daemonset)
if err != nil {
return err
}
err = daemonsetcontroller.federatedApiClient.Extensions().DaemonSets(daemonset.Namespace).Delete(daemonset.Name, nil)
if err != nil {
// Its all good if the error is not found error. That means it is deleted already and we do not have to do anything.
// This is expected when we are processing an update as a result of daemonset finalizer deletion.
// The process that deleted the last finalizer is also going to delete the daemonset and we do not have to do anything.
if !errors.IsNotFound(err) {
return fmt.Errorf("failed to delete daemonset: %v", err)
}
}
return nil
}

View file

@ -0,0 +1,158 @@
/*
Copyright 2016 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 daemonset
import (
"fmt"
"reflect"
"testing"
"time"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
fakefedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5/fake"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/federation/pkg/federation-controller/util/deletionhelper"
. "k8s.io/kubernetes/federation/pkg/federation-controller/util/test"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
extensionsv1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
fakekubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5/fake"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/wait"
"github.com/stretchr/testify/assert"
)
func TestDaemonSetController(t *testing.T) {
cluster1 := NewCluster("cluster1", apiv1.ConditionTrue)
cluster2 := NewCluster("cluster2", apiv1.ConditionTrue)
fakeClient := &fakefedclientset.Clientset{}
RegisterFakeList("clusters", &fakeClient.Fake, &federationapi.ClusterList{Items: []federationapi.Cluster{*cluster1}})
RegisterFakeList("daemonsets", &fakeClient.Fake, &extensionsv1.DaemonSetList{Items: []extensionsv1.DaemonSet{}})
daemonsetWatch := RegisterFakeWatch("daemonsets", &fakeClient.Fake)
daemonsetUpdateChan := RegisterFakeCopyOnUpdate("daemonsets", &fakeClient.Fake, daemonsetWatch)
clusterWatch := RegisterFakeWatch("clusters", &fakeClient.Fake)
cluster1Client := &fakekubeclientset.Clientset{}
cluster1Watch := RegisterFakeWatch("daemonsets", &cluster1Client.Fake)
RegisterFakeList("daemonsets", &cluster1Client.Fake, &extensionsv1.DaemonSetList{Items: []extensionsv1.DaemonSet{}})
cluster1CreateChan := RegisterFakeCopyOnCreate("daemonsets", &cluster1Client.Fake, cluster1Watch)
cluster1UpdateChan := RegisterFakeCopyOnUpdate("daemonsets", &cluster1Client.Fake, cluster1Watch)
cluster2Client := &fakekubeclientset.Clientset{}
cluster2Watch := RegisterFakeWatch("daemonsets", &cluster2Client.Fake)
RegisterFakeList("daemonsets", &cluster2Client.Fake, &extensionsv1.DaemonSetList{Items: []extensionsv1.DaemonSet{}})
cluster2CreateChan := RegisterFakeCopyOnCreate("daemonsets", &cluster2Client.Fake, cluster2Watch)
daemonsetController := NewDaemonSetController(fakeClient)
informer := ToFederatedInformerForTestOnly(daemonsetController.daemonsetFederatedInformer)
informer.SetClientFactory(func(cluster *federationapi.Cluster) (kubeclientset.Interface, error) {
switch cluster.Name {
case cluster1.Name:
return cluster1Client, nil
case cluster2.Name:
return cluster2Client, nil
default:
return nil, fmt.Errorf("Unknown cluster")
}
})
daemonsetController.clusterAvailableDelay = time.Second
daemonsetController.daemonsetReviewDelay = 50 * time.Millisecond
daemonsetController.smallDelay = 20 * time.Millisecond
daemonsetController.updateTimeout = 5 * time.Second
stop := make(chan struct{})
daemonsetController.Run(stop)
daemonset1 := extensionsv1.DaemonSet{
ObjectMeta: apiv1.ObjectMeta{
Name: "test-daemonset",
Namespace: "ns",
SelfLink: "/api/v1/namespaces/ns/daemonsets/test-daemonset",
},
Spec: extensionsv1.DaemonSetSpec{
Selector: &metav1.LabelSelector{
MatchLabels: make(map[string]string),
},
},
}
// Test add federated daemonset.
daemonsetWatch.Add(&daemonset1)
// There should be 2 updates to add both the finalizers.
updatedDaemonSet := GetDaemonSetFromChan(daemonsetUpdateChan)
assert.True(t, daemonsetController.hasFinalizerFunc(updatedDaemonSet, deletionhelper.FinalizerDeleteFromUnderlyingClusters))
updatedDaemonSet = GetDaemonSetFromChan(daemonsetUpdateChan)
assert.True(t, daemonsetController.hasFinalizerFunc(updatedDaemonSet, apiv1.FinalizerOrphan))
daemonset1 = *updatedDaemonSet
createdDaemonSet := GetDaemonSetFromChan(cluster1CreateChan)
assert.NotNil(t, createdDaemonSet)
assert.Equal(t, daemonset1.Namespace, createdDaemonSet.Namespace)
assert.Equal(t, daemonset1.Name, createdDaemonSet.Name)
assert.True(t, daemonsetsEqual(daemonset1, *createdDaemonSet),
fmt.Sprintf("expected: %v, actual: %v", daemonset1, *createdDaemonSet))
// Wait for the daemonset to appear in the informer store
err := WaitForStoreUpdate(
daemonsetController.daemonsetFederatedInformer.GetTargetStore(),
cluster1.Name, getDaemonSetKey(daemonset1.Namespace, daemonset1.Name), wait.ForeverTestTimeout)
assert.Nil(t, err, "daemonset should have appeared in the informer store")
// TODO: Re-enable this when we have fixed these flaky tests: https://github.com/kubernetes/kubernetes/issues/36540.
// Test update federated daemonset.
daemonset1.Annotations = map[string]string{
"A": "B",
}
daemonsetWatch.Modify(&daemonset1)
updatedDaemonSet = GetDaemonSetFromChan(cluster1UpdateChan)
assert.NotNil(t, updatedDaemonSet)
assert.Equal(t, daemonset1.Name, updatedDaemonSet.Name)
assert.Equal(t, daemonset1.Namespace, updatedDaemonSet.Namespace)
assert.True(t, daemonsetsEqual(daemonset1, *updatedDaemonSet),
fmt.Sprintf("expected: %v, actual: %v", daemonset1, *updatedDaemonSet))
// Test update federated daemonset.
daemonset1.Spec.Template.Name = "TEST"
daemonsetWatch.Modify(&daemonset1)
err = CheckObjectFromChan(cluster1UpdateChan, MetaAndSpecCheckingFunction(&daemonset1))
assert.NoError(t, err)
// Test add cluster
clusterWatch.Add(cluster2)
createdDaemonSet2 := GetDaemonSetFromChan(cluster2CreateChan)
assert.NotNil(t, createdDaemonSet2)
assert.Equal(t, daemonset1.Name, createdDaemonSet2.Name)
assert.Equal(t, daemonset1.Namespace, createdDaemonSet2.Namespace)
assert.True(t, daemonsetsEqual(daemonset1, *createdDaemonSet2),
fmt.Sprintf("expected: %v, actual: %v", daemonset1, *createdDaemonSet2))
close(stop)
}
func daemonsetsEqual(a, b extensionsv1.DaemonSet) bool {
return util.ObjectMetaEquivalent(a.ObjectMeta, b.ObjectMeta) && reflect.DeepEqual(a.Spec, b.Spec)
}
func GetDaemonSetFromChan(c chan runtime.Object) *extensionsv1.DaemonSet {
daemonset := GetObjectFromChan(c).(*extensionsv1.DaemonSet)
return daemonset
}

View file

@ -0,0 +1,63 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["deploymentcontroller.go"],
tags = ["automanaged"],
deps = [
"//federation/apis/federation:go_default_library",
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/deletionhelper:go_default_library",
"//federation/pkg/federation-controller/util/eventsink:go_default_library",
"//federation/pkg/federation-controller/util/planner:go_default_library",
"//federation/pkg/federation-controller/util/podanalyzer:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/errors:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/record:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/conversion:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/util/flowcontrol:go_default_library",
"//pkg/util/wait:go_default_library",
"//pkg/util/workqueue:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = ["deploymentcontroller_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5/fake:go_default_library",
"//federation/pkg/federation-controller/util/test:go_default_library",
"//pkg/api/meta:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/clientset_generated/release_1_5/fake:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/types:go_default_library",
"//pkg/util/wait:go_default_library",
"//vendor:github.com/stretchr/testify/assert",
],
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,186 @@
/*
Copyright 2016 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 deployment
import (
"flag"
"fmt"
"testing"
"time"
fedv1 "k8s.io/kubernetes/federation/apis/federation/v1beta1"
fakefedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5/fake"
. "k8s.io/kubernetes/federation/pkg/federation-controller/util/test"
"k8s.io/kubernetes/pkg/api/meta"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
extensionsv1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
fakekubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5/fake"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/wait"
"github.com/stretchr/testify/assert"
)
func TestParseFederationDeploymentPreference(t *testing.T) {
successPrefs := []string{
`{"rebalance": true,
"clusters": {
"k8s-1": {"minReplicas": 10, "maxReplicas": 20, "weight": 2},
"*": {"weight": 1}
}}`,
}
failedPrefes := []string{
`{`, // bad json
}
rs := newDeploymentWithReplicas("d-1", 100)
accessor, _ := meta.Accessor(rs)
anno := accessor.GetAnnotations()
if anno == nil {
anno = make(map[string]string)
accessor.SetAnnotations(anno)
}
for _, prefString := range successPrefs {
anno[FedDeploymentPreferencesAnnotation] = prefString
pref, err := parseFederationDeploymentPreference(rs)
assert.NotNil(t, pref)
assert.Nil(t, err)
}
for _, prefString := range failedPrefes {
anno[FedDeploymentPreferencesAnnotation] = prefString
pref, err := parseFederationDeploymentPreference(rs)
assert.Nil(t, pref)
assert.NotNil(t, err)
}
}
func TestDeploymentController(t *testing.T) {
flag.Set("logtostderr", "true")
flag.Set("v", "5")
flag.Parse()
deploymentReviewDelay = 500 * time.Millisecond
clusterAvailableDelay = 100 * time.Millisecond
clusterUnavailableDelay = 100 * time.Millisecond
allDeploymentReviewDelay = 500 * time.Millisecond
cluster1 := NewCluster("cluster1", apiv1.ConditionTrue)
cluster2 := NewCluster("cluster2", apiv1.ConditionTrue)
fakeClient := &fakefedclientset.Clientset{}
RegisterFakeList("clusters", &fakeClient.Fake, &fedv1.ClusterList{Items: []fedv1.Cluster{*cluster1}})
deploymentsWatch := RegisterFakeWatch("deployments", &fakeClient.Fake)
clusterWatch := RegisterFakeWatch("clusters", &fakeClient.Fake)
cluster1Client := &fakekubeclientset.Clientset{}
cluster1Watch := RegisterFakeWatch("deployments", &cluster1Client.Fake)
_ = RegisterFakeWatch("pods", &cluster1Client.Fake)
RegisterFakeList("deployments", &cluster1Client.Fake, &extensionsv1.DeploymentList{Items: []extensionsv1.Deployment{}})
cluster1CreateChan := RegisterFakeCopyOnCreate("deployments", &cluster1Client.Fake, cluster1Watch)
cluster1UpdateChan := RegisterFakeCopyOnUpdate("deployments", &cluster1Client.Fake, cluster1Watch)
cluster2Client := &fakekubeclientset.Clientset{}
cluster2Watch := RegisterFakeWatch("deployments", &cluster2Client.Fake)
_ = RegisterFakeWatch("pods", &cluster2Client.Fake)
RegisterFakeList("deployments", &cluster2Client.Fake, &extensionsv1.DeploymentList{Items: []extensionsv1.Deployment{}})
cluster2CreateChan := RegisterFakeCopyOnCreate("deployments", &cluster2Client.Fake, cluster2Watch)
deploymentController := NewDeploymentController(fakeClient)
clientFactory := func(cluster *fedv1.Cluster) (kubeclientset.Interface, error) {
switch cluster.Name {
case cluster1.Name:
return cluster1Client, nil
case cluster2.Name:
return cluster2Client, nil
default:
return nil, fmt.Errorf("Unknown cluster")
}
}
ToFederatedInformerForTestOnly(deploymentController.fedDeploymentInformer).SetClientFactory(clientFactory)
ToFederatedInformerForTestOnly(deploymentController.fedPodInformer).SetClientFactory(clientFactory)
stop := make(chan struct{})
go deploymentController.Run(5, stop)
// Create deployment. Expect to see it in cluster1.
dep1 := newDeploymentWithReplicas("depA", 6)
deploymentsWatch.Add(dep1)
checkDeployment := func(base *extensionsv1.Deployment, replicas int32) CheckingFunction {
return func(obj runtime.Object) error {
if obj == nil {
return fmt.Errorf("Observed object is nil")
}
d := obj.(*extensionsv1.Deployment)
if err := CompareObjectMeta(base.ObjectMeta, d.ObjectMeta); err != nil {
return err
}
if replicas != *d.Spec.Replicas {
return fmt.Errorf("Replica count is different expected:%d observed:%d", replicas, *d.Spec.Replicas)
}
return nil
}
}
assert.NoError(t, CheckObjectFromChan(cluster1CreateChan, checkDeployment(dep1, *dep1.Spec.Replicas)))
err := WaitForStoreUpdate(
deploymentController.fedDeploymentInformer.GetTargetStore(),
cluster1.Name, types.NamespacedName{Namespace: dep1.Namespace, Name: dep1.Name}.String(), wait.ForeverTestTimeout)
assert.Nil(t, err, "deployment should have appeared in the informer store")
// Increase replica count. Expect to see the update in cluster1.
newRep := int32(8)
dep1.Spec.Replicas = &newRep
deploymentsWatch.Modify(dep1)
assert.NoError(t, CheckObjectFromChan(cluster1UpdateChan, checkDeployment(dep1, *dep1.Spec.Replicas)))
// Add new cluster. Although rebalance = false, no pods have been created yet so it should
// rebalance anyway.
clusterWatch.Add(cluster2)
assert.NoError(t, CheckObjectFromChan(cluster1UpdateChan, checkDeployment(dep1, *dep1.Spec.Replicas/2)))
assert.NoError(t, CheckObjectFromChan(cluster2CreateChan, checkDeployment(dep1, *dep1.Spec.Replicas/2)))
// Add new deployment with non-default replica placement preferences.
dep2 := newDeploymentWithReplicas("deployment2", 9)
dep2.Annotations = make(map[string]string)
dep2.Annotations[FedDeploymentPreferencesAnnotation] = `{"rebalance": true,
"clusters": {
"cluster1": {"weight": 2},
"cluster2": {"weight": 1}
}}`
deploymentsWatch.Add(dep2)
assert.NoError(t, CheckObjectFromChan(cluster1CreateChan, checkDeployment(dep2, 6)))
assert.NoError(t, CheckObjectFromChan(cluster2CreateChan, checkDeployment(dep2, 3)))
}
func GetDeploymentFromChan(c chan runtime.Object) *extensionsv1.Deployment {
secret := GetObjectFromChan(c).(*extensionsv1.Deployment)
return secret
}
func newDeploymentWithReplicas(name string, replicas int32) *extensionsv1.Deployment {
return &extensionsv1.Deployment{
ObjectMeta: apiv1.ObjectMeta{
Name: name,
Namespace: apiv1.NamespaceDefault,
SelfLink: "/api/v1/namespaces/default/deployments/name",
},
Spec: extensionsv1.DeploymentSpec{
Replicas: &replicas,
},
}
}

View file

@ -0,0 +1,19 @@
/*
Copyright 2015 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 federation_controller contains code for controllers (like the cluster
// controller).
package federation_controller // import "k8s.io/kubernetes/federation/pkg/federation-controller"

View file

@ -0,0 +1,62 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["ingress_controller.go"],
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/deletionhelper:go_default_library",
"//federation/pkg/federation-controller/util/eventsink:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/errors:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/record:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/conversion:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/types:go_default_library",
"//pkg/util/flowcontrol:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = ["ingress_controller_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5/fake:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/deletionhelper:go_default_library",
"//federation/pkg/federation-controller/util/test:go_default_library",
"//pkg/api/errors:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/clientset_generated/release_1_5/fake:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/types:go_default_library",
"//pkg/util/wait:go_default_library",
"//vendor:github.com/stretchr/testify/assert",
],
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,294 @@
/*
Copyright 2016 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 ingress
import (
"fmt"
"reflect"
"testing"
"time"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
fakefedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5/fake"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/federation/pkg/federation-controller/util/deletionhelper"
. "k8s.io/kubernetes/federation/pkg/federation-controller/util/test"
"k8s.io/kubernetes/pkg/api/errors"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
"k8s.io/kubernetes/pkg/client/cache"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
fakekubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5/fake"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/wait"
"github.com/stretchr/testify/assert"
)
func TestIngressController(t *testing.T) {
fakeClusterList := federationapi.ClusterList{Items: []federationapi.Cluster{}}
fakeConfigMapList1 := apiv1.ConfigMapList{Items: []apiv1.ConfigMap{}}
fakeConfigMapList2 := apiv1.ConfigMapList{Items: []apiv1.ConfigMap{}}
cluster1 := NewCluster("cluster1", apiv1.ConditionTrue)
cluster2 := NewCluster("cluster2", apiv1.ConditionTrue)
cfg1 := NewConfigMap("foo")
cfg2 := NewConfigMap("bar") // Different UID from cfg1, so that we can check that they get reconciled.
t.Log("Creating fake infrastructure")
fedClient := &fakefedclientset.Clientset{}
RegisterFakeList("clusters", &fedClient.Fake, &fakeClusterList)
RegisterFakeList("ingresses", &fedClient.Fake, &extensionsv1beta1.IngressList{Items: []extensionsv1beta1.Ingress{}})
fedIngressWatch := RegisterFakeWatch("ingresses", &fedClient.Fake)
clusterWatch := RegisterFakeWatch("clusters", &fedClient.Fake)
fedClusterUpdateChan := RegisterFakeCopyOnUpdate("clusters", &fedClient.Fake, clusterWatch)
//fedIngressUpdateChan := RegisterFakeCopyOnUpdate("ingresses", &fedClient.Fake, fedIngressWatch)
cluster1Client := &fakekubeclientset.Clientset{}
RegisterFakeList("ingresses", &cluster1Client.Fake, &extensionsv1beta1.IngressList{Items: []extensionsv1beta1.Ingress{}})
RegisterFakeList("configmaps", &cluster1Client.Fake, &fakeConfigMapList1)
cluster1IngressWatch := RegisterFakeWatch("ingresses", &cluster1Client.Fake)
cluster1ConfigMapWatch := RegisterFakeWatch("configmaps", &cluster1Client.Fake)
cluster1IngressCreateChan := RegisterFakeCopyOnCreate("ingresses", &cluster1Client.Fake, cluster1IngressWatch)
// cluster1IngressUpdateChan := RegisterFakeCopyOnUpdate("ingresses", &cluster1Client.Fake, cluster1IngressWatch)
cluster2Client := &fakekubeclientset.Clientset{}
RegisterFakeList("ingresses", &cluster2Client.Fake, &extensionsv1beta1.IngressList{Items: []extensionsv1beta1.Ingress{}})
RegisterFakeList("configmaps", &cluster2Client.Fake, &fakeConfigMapList2)
cluster2IngressWatch := RegisterFakeWatch("ingresses", &cluster2Client.Fake)
cluster2ConfigMapWatch := RegisterFakeWatch("configmaps", &cluster2Client.Fake)
cluster2IngressCreateChan := RegisterFakeCopyOnCreate("ingresses", &cluster2Client.Fake, cluster2IngressWatch)
cluster2ConfigMapUpdateChan := RegisterFakeCopyOnUpdate("configmaps", &cluster2Client.Fake, cluster2ConfigMapWatch)
clientFactoryFunc := func(cluster *federationapi.Cluster) (kubeclientset.Interface, error) {
switch cluster.Name {
case cluster1.Name:
return cluster1Client, nil
case cluster2.Name:
return cluster2Client, nil
default:
return nil, fmt.Errorf("Unknown cluster")
}
}
ingressController := NewIngressController(fedClient)
ingressInformer := ToFederatedInformerForTestOnly(ingressController.ingressFederatedInformer)
ingressInformer.SetClientFactory(clientFactoryFunc)
configMapInformer := ToFederatedInformerForTestOnly(ingressController.configMapFederatedInformer)
configMapInformer.SetClientFactory(clientFactoryFunc)
ingressController.clusterAvailableDelay = time.Second
ingressController.ingressReviewDelay = 10 * time.Millisecond
ingressController.configMapReviewDelay = 10 * time.Millisecond
ingressController.smallDelay = 20 * time.Millisecond
ingressController.updateTimeout = 5 * time.Second
stop := make(chan struct{})
t.Log("Running Ingress Controller")
ingressController.Run(stop)
// TODO: Here we are creating the ingress with first cluster annotation.
// Add another test without that annotation when
// https://github.com/kubernetes/kubernetes/issues/36540 is fixed.
ing1 := extensionsv1beta1.Ingress{
ObjectMeta: apiv1.ObjectMeta{
Name: "test-ingress",
Namespace: "mynamespace",
SelfLink: "/api/v1/namespaces/mynamespace/ingress/test-ingress",
Annotations: map[string]string{
firstClusterAnnotation: cluster1.Name,
},
},
Status: extensionsv1beta1.IngressStatus{
LoadBalancer: apiv1.LoadBalancerStatus{
Ingress: make([]apiv1.LoadBalancerIngress, 0, 0),
},
},
}
t.Log("Adding cluster 1")
clusterWatch.Add(cluster1)
t.Log("Adding Ingress UID ConfigMap to cluster 1")
cluster1ConfigMapWatch.Add(cfg1)
t.Log("Checking that UID annotation on Cluster 1 annotation was correctly updated")
cluster := GetClusterFromChan(fedClusterUpdateChan)
assert.NotNil(t, cluster)
assert.Equal(t, cluster.ObjectMeta.Annotations[uidAnnotationKey], cfg1.Data[uidKey])
// Test add federated ingress.
t.Log("Adding Federated Ingress")
fedIngressWatch.Add(&ing1)
/*
// TODO: Re-enable this when we have fixed these flaky tests: https://github.com/kubernetes/kubernetes/issues/36540.
t.Logf("Checking that approproate finalizers are added")
// There should be 2 updates to add both the finalizers.
updatedIngress := GetIngressFromChan(t, fedIngressUpdateChan)
assert.True(t, ingressController.hasFinalizerFunc(updatedIngress, deletionhelper.FinalizerDeleteFromUnderlyingClusters))
updatedIngress = GetIngressFromChan(t, fedIngressUpdateChan)
assert.True(t, ingressController.hasFinalizerFunc(updatedIngress, apiv1.FinalizerOrphan), fmt.Sprintf("ingress does not have the orphan finalizer: %v", updatedIngress))
ing1 = *updatedIngress
*/
t.Log("Checking that Ingress was correctly created in cluster 1")
createdIngress := GetIngressFromChan(t, cluster1IngressCreateChan)
assert.NotNil(t, createdIngress)
assert.True(t, reflect.DeepEqual(ing1.Spec, createdIngress.Spec), "Spec of created ingress is not equal")
assert.True(t, util.ObjectMetaEquivalent(ing1.ObjectMeta, createdIngress.ObjectMeta), "Metadata of created object is not equivalent")
// Wait for finalizers to appear in federation store.
// assert.NoError(t, WaitForFinalizersInFederationStore(ingressController, ingressController.ingressInformerStore,
// types.NamespacedName{Namespace: ing1.Namespace, Name: ing1.Name}.String()), "finalizers not found in federated ingress")
// Wait for the cluster ingress to appear in cluster store.
assert.NoError(t, WaitForIngressInClusterStore(ingressController.ingressFederatedInformer.GetTargetStore(), cluster1.Name,
types.NamespacedName{Namespace: createdIngress.Namespace, Name: createdIngress.Name}.String()),
"Created ingress not found in underlying cluster store")
/*
// TODO: Re-enable this when we have fixed these flaky tests: https://github.com/kubernetes/kubernetes/issues/36540.
// Test that IP address gets transferred from cluster ingress to federated ingress.
t.Log("Checking that IP address gets transferred from cluster ingress to federated ingress")
createdIngress.Status.LoadBalancer.Ingress = append(createdIngress.Status.LoadBalancer.Ingress, apiv1.LoadBalancerIngress{IP: "1.2.3.4"})
cluster1IngressWatch.Modify(createdIngress)
// Wait for store to see the updated cluster ingress.
assert.NoError(t, WaitForStatusUpdate(t, ingressController.ingressFederatedInformer.GetTargetStore(),
cluster1.Name, types.NamespacedName{Namespace: createdIngress.Namespace, Name: createdIngress.Name}.String(),
createdIngress.Status.LoadBalancer, 4*wait.ForeverTestTimeout))
updatedIngress = GetIngressFromChan(t, fedIngressUpdateChan)
assert.NotNil(t, updatedIngress, "Cluster's ingress load balancer status was not correctly transferred to the federated ingress")
if updatedIngress != nil {
assert.True(t, reflect.DeepEqual(createdIngress.Status.LoadBalancer.Ingress, updatedIngress.Status.LoadBalancer.Ingress), fmt.Sprintf("Ingress IP was not transferred from cluster ingress to federated ingress. %v is not equal to %v", createdIngress.Status.LoadBalancer.Ingress, updatedIngress.Status.LoadBalancer.Ingress))
t.Logf("expected: %v, actual: %v", createdIngress, updatedIngress)
}
// Test update federated ingress.
if updatedIngress.ObjectMeta.Annotations == nil {
updatedIngress.ObjectMeta.Annotations = make(map[string]string)
}
updatedIngress.ObjectMeta.Annotations["A"] = "B"
t.Log("Modifying Federated Ingress")
fedIngressWatch.Modify(updatedIngress)
t.Log("Checking that Ingress was correctly updated in cluster 1")
updatedIngress2 := GetIngressFromChan(t, cluster1IngressUpdateChan)
assert.NotNil(t, updatedIngress2)
assert.True(t, reflect.DeepEqual(updatedIngress2.Spec, updatedIngress.Spec), "Spec of updated ingress is not equal")
assert.Equal(t, updatedIngress2.ObjectMeta.Annotations["A"], updatedIngress.ObjectMeta.Annotations["A"], "Updated annotation not transferred from federated to cluster ingress.")
*/
// Test add cluster
t.Log("Adding a second cluster")
ing1.Annotations[staticIPNameKeyWritable] = "foo" // Make sure that the base object has a static IP name first.
fedIngressWatch.Modify(&ing1)
clusterWatch.Add(cluster2)
// First check that the original values are not equal - see above comment
assert.NotEqual(t, cfg1.Data[uidKey], cfg2.Data[uidKey], fmt.Sprintf("ConfigMap in cluster 2 must initially not equal that in cluster 1 for this test - please fix test"))
cluster2ConfigMapWatch.Add(cfg2)
t.Log("Checking that the ingress got created in cluster 2")
createdIngress2 := GetIngressFromChan(t, cluster2IngressCreateChan)
assert.NotNil(t, createdIngress2)
assert.True(t, reflect.DeepEqual(ing1.Spec, createdIngress2.Spec), "Spec of created ingress is not equal")
assert.True(t, util.ObjectMetaEquivalent(ing1.ObjectMeta, createdIngress2.ObjectMeta), "Metadata of created object is not equivalent")
t.Log("Checking that the configmap in cluster 2 got updated.")
updatedConfigMap2 := GetConfigMapFromChan(cluster2ConfigMapUpdateChan)
assert.NotNil(t, updatedConfigMap2, fmt.Sprintf("ConfigMap in cluster 2 was not updated (or more likely the test is broken and the API type written is wrong)"))
if updatedConfigMap2 != nil {
assert.Equal(t, cfg1.Data[uidKey], updatedConfigMap2.Data[uidKey],
fmt.Sprintf("UID's in configmaps in cluster's 1 and 2 are not equal (%q != %q)", cfg1.Data["uid"], updatedConfigMap2.Data["uid"]))
}
close(stop)
}
func GetIngressFromChan(t *testing.T, c chan runtime.Object) *extensionsv1beta1.Ingress {
obj := GetObjectFromChan(c)
ingress, ok := obj.(*extensionsv1beta1.Ingress)
if !ok {
t.Logf("Object on channel was not of type *extensionsv1beta1.Ingress: %v", obj)
}
return ingress
}
func GetConfigMapFromChan(c chan runtime.Object) *apiv1.ConfigMap {
configMap, _ := GetObjectFromChan(c).(*apiv1.ConfigMap)
return configMap
}
func GetClusterFromChan(c chan runtime.Object) *federationapi.Cluster {
cluster, _ := GetObjectFromChan(c).(*federationapi.Cluster)
return cluster
}
func NewConfigMap(uid string) *apiv1.ConfigMap {
return &apiv1.ConfigMap{
ObjectMeta: apiv1.ObjectMeta{
Name: uidConfigMapName,
Namespace: uidConfigMapNamespace,
SelfLink: "/api/v1/namespaces/" + uidConfigMapNamespace + "/configmap/" + uidConfigMapName,
// TODO: Remove: Annotations: map[string]string{},
},
Data: map[string]string{
uidKey: uid,
},
}
}
// Wait for finalizers to appear in federation store.
func WaitForFinalizersInFederationStore(ingressController *IngressController, store cache.Store, key string) error {
retryInterval := 100 * time.Millisecond
timeout := wait.ForeverTestTimeout
err := wait.PollImmediate(retryInterval, timeout, func() (bool, error) {
obj, found, err := store.GetByKey(key)
if !found || err != nil {
return false, err
}
ingress := obj.(*extensionsv1beta1.Ingress)
if ingressController.hasFinalizerFunc(ingress, apiv1.FinalizerOrphan) &&
ingressController.hasFinalizerFunc(ingress, deletionhelper.FinalizerDeleteFromUnderlyingClusters) {
return true, nil
}
return false, nil
})
return err
}
// Wait for the cluster ingress to appear in cluster store.
func WaitForIngressInClusterStore(store util.FederatedReadOnlyStore, clusterName, key string) error {
retryInterval := 100 * time.Millisecond
timeout := wait.ForeverTestTimeout
err := wait.PollImmediate(retryInterval, timeout, func() (bool, error) {
_, found, err := store.GetByKey(clusterName, key)
if found && err == nil {
return true, nil
}
if errors.IsNotFound(err) {
return false, nil
}
return false, err
})
return err
}
// Wait for ingress status to be updated to match the desiredStatus.
func WaitForStatusUpdate(t *testing.T, store util.FederatedReadOnlyStore, clusterName, key string, desiredStatus apiv1.LoadBalancerStatus, timeout time.Duration) error {
retryInterval := 100 * time.Millisecond
err := wait.PollImmediate(retryInterval, timeout, func() (bool, error) {
obj, found, err := store.GetByKey(clusterName, key)
if !found || err != nil {
return false, err
}
ingress := obj.(*extensionsv1beta1.Ingress)
return reflect.DeepEqual(ingress.Status.LoadBalancer, desiredStatus), nil
})
return err
}

View file

@ -0,0 +1,59 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["namespace_controller.go"],
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/deletionhelper:go_default_library",
"//federation/pkg/federation-controller/util/eventsink:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/errors:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/record:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/conversion:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/util/flowcontrol:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = ["namespace_controller_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5/fake:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/deletionhelper:go_default_library",
"//federation/pkg/federation-controller/util/test:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/clientset_generated/release_1_5/fake:go_default_library",
"//pkg/client/testing/core:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/util/wait:go_default_library",
"//vendor:github.com/stretchr/testify/assert",
],
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,206 @@
/*
Copyright 2016 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 namespace
import (
"fmt"
"testing"
"time"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
fakefedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5/fake"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/federation/pkg/federation-controller/util/deletionhelper"
. "k8s.io/kubernetes/federation/pkg/federation-controller/util/test"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
extensionsv1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
fakekubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5/fake"
"k8s.io/kubernetes/pkg/client/testing/core"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/wait"
"github.com/stretchr/testify/assert"
)
func TestNamespaceController(t *testing.T) {
cluster1 := NewCluster("cluster1", apiv1.ConditionTrue)
cluster2 := NewCluster("cluster2", apiv1.ConditionTrue)
ns1 := apiv1.Namespace{
ObjectMeta: apiv1.ObjectMeta{
Name: "test-namespace",
SelfLink: "/api/v1/namespaces/test-namespace",
},
Spec: apiv1.NamespaceSpec{
Finalizers: []apiv1.FinalizerName{apiv1.FinalizerKubernetes},
},
}
fakeClient := &fakefedclientset.Clientset{}
RegisterFakeList("clusters", &fakeClient.Fake, &federationapi.ClusterList{Items: []federationapi.Cluster{*cluster1}})
RegisterFakeList("namespaces", &fakeClient.Fake, &apiv1.NamespaceList{Items: []apiv1.Namespace{}})
namespaceWatch := RegisterFakeWatch("namespaces", &fakeClient.Fake)
namespaceCreateChan := RegisterFakeCopyOnCreate("namespaces", &fakeClient.Fake, namespaceWatch)
clusterWatch := RegisterFakeWatch("clusters", &fakeClient.Fake)
cluster1Client := &fakekubeclientset.Clientset{}
cluster1Watch := RegisterFakeWatch("namespaces", &cluster1Client.Fake)
RegisterFakeList("namespaces", &cluster1Client.Fake, &apiv1.NamespaceList{Items: []apiv1.Namespace{}})
cluster1CreateChan := RegisterFakeCopyOnCreate("namespaces", &cluster1Client.Fake, cluster1Watch)
// cluster1UpdateChan := RegisterFakeCopyOnUpdate("namespaces", &cluster1Client.Fake, cluster1Watch)
cluster2Client := &fakekubeclientset.Clientset{}
cluster2Watch := RegisterFakeWatch("namespaces", &cluster2Client.Fake)
RegisterFakeList("namespaces", &cluster2Client.Fake, &apiv1.NamespaceList{Items: []apiv1.Namespace{}})
cluster2CreateChan := RegisterFakeCopyOnCreate("namespaces", &cluster2Client.Fake, cluster2Watch)
RegisterFakeList("replicasets", &fakeClient.Fake, &extensionsv1.ReplicaSetList{Items: []extensionsv1.ReplicaSet{
{
ObjectMeta: apiv1.ObjectMeta{
Name: "test-rs",
Namespace: ns1.Namespace,
}}}})
RegisterFakeList("secrets", &fakeClient.Fake, &apiv1.SecretList{Items: []apiv1.Secret{
{
ObjectMeta: apiv1.ObjectMeta{
Name: "test-secret",
Namespace: ns1.Namespace,
}}}})
RegisterFakeList("services", &fakeClient.Fake, &apiv1.ServiceList{Items: []apiv1.Service{
{
ObjectMeta: apiv1.ObjectMeta{
Name: "test-service",
Namespace: ns1.Namespace,
}}}})
nsDeleteChan := RegisterDelete(&fakeClient.Fake, "namespaces")
rsDeleteChan := RegisterDeleteCollection(&fakeClient.Fake, "replicasets")
serviceDeleteChan := RegisterDeleteCollection(&fakeClient.Fake, "services")
secretDeleteChan := RegisterDeleteCollection(&fakeClient.Fake, "secrets")
namespaceController := NewNamespaceController(fakeClient)
informerClientFactory := func(cluster *federationapi.Cluster) (kubeclientset.Interface, error) {
switch cluster.Name {
case cluster1.Name:
return cluster1Client, nil
case cluster2.Name:
return cluster2Client, nil
default:
return nil, fmt.Errorf("Unknown cluster")
}
}
setClientFactory(namespaceController.namespaceFederatedInformer, informerClientFactory)
namespaceController.clusterAvailableDelay = time.Second
namespaceController.namespaceReviewDelay = 50 * time.Millisecond
namespaceController.smallDelay = 20 * time.Millisecond
namespaceController.updateTimeout = 5 * time.Second
stop := make(chan struct{})
namespaceController.Run(stop)
// Test add federated namespace.
namespaceWatch.Add(&ns1)
// Verify that the DeleteFromUnderlyingClusters finalizer is added to the namespace.
// Note: finalize invokes the create action in Fake client.
// TODO: Seems like a bug. Should invoke update. Fix it.
updatedNamespace := GetNamespaceFromChan(namespaceCreateChan)
assert.True(t, namespaceController.hasFinalizerFunc(updatedNamespace, deletionhelper.FinalizerDeleteFromUnderlyingClusters))
ns1 = *updatedNamespace
// Verify that the namespace is created in underlying cluster1.
createdNamespace := GetNamespaceFromChan(cluster1CreateChan)
assert.NotNil(t, createdNamespace)
assert.Equal(t, ns1.Name, createdNamespace.Name)
// Wait for the namespace to appear in the informer store
err := WaitForStoreUpdate(
namespaceController.namespaceFederatedInformer.GetTargetStore(),
cluster1.Name, ns1.Name, wait.ForeverTestTimeout)
assert.Nil(t, err, "namespace should have appeared in the informer store")
/*
// TODO: Uncomment this once we have figured out why this is flaky.
// Test update federated namespace.
ns1.Annotations = map[string]string{
"A": "B",
}
namespaceWatch.Modify(&ns1)
updatedNamespace = GetNamespaceFromChan(cluster1UpdateChan)
assert.NotNil(t, updatedNamespace)
assert.Equal(t, ns1.Name, updatedNamespace.Name)
// assert.Contains(t, updatedNamespace.Annotations, "A")
*/
// Test add cluster
clusterWatch.Add(cluster2)
createdNamespace2 := GetNamespaceFromChan(cluster2CreateChan)
assert.NotNil(t, createdNamespace2)
assert.Equal(t, ns1.Name, createdNamespace2.Name)
// assert.Contains(t, createdNamespace2.Annotations, "A")
// Delete the namespace with orphan finalizer (let namespaces
// in underlying clusters be as is).
// TODO: Add a test without orphan finalizer.
ns1.ObjectMeta.Finalizers = append(ns1.ObjectMeta.Finalizers, apiv1.FinalizerOrphan)
ns1.DeletionTimestamp = &metav1.Time{Time: time.Now()}
namespaceWatch.Modify(&ns1)
assert.Equal(t, ns1.Name, GetStringFromChan(nsDeleteChan))
assert.Equal(t, "all", GetStringFromChan(rsDeleteChan))
assert.Equal(t, "all", GetStringFromChan(serviceDeleteChan))
assert.Equal(t, "all", GetStringFromChan(secretDeleteChan))
close(stop)
}
func setClientFactory(informer util.FederatedInformer, informerClientFactory func(*federationapi.Cluster) (kubeclientset.Interface, error)) {
testInformer := ToFederatedInformerForTestOnly(informer)
testInformer.SetClientFactory(informerClientFactory)
}
func RegisterDeleteCollection(client *core.Fake, resource string) chan string {
deleteChan := make(chan string, 100)
client.AddReactor("delete-collection", resource, func(action core.Action) (bool, runtime.Object, error) {
deleteChan <- "all"
return true, nil, nil
})
return deleteChan
}
func RegisterDelete(client *core.Fake, resource string) chan string {
deleteChan := make(chan string, 100)
client.AddReactor("delete", resource, func(action core.Action) (bool, runtime.Object, error) {
deleteAction := action.(core.DeleteAction)
deleteChan <- deleteAction.GetName()
return true, nil, nil
})
return deleteChan
}
func GetStringFromChan(c chan string) string {
select {
case str := <-c:
return str
case <-time.After(5 * time.Second):
return "timedout"
}
}
func GetNamespaceFromChan(c chan runtime.Object) *apiv1.Namespace {
namespace := GetObjectFromChan(c).(*apiv1.Namespace)
return namespace
}

View file

@ -0,0 +1,62 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["replicasetcontroller.go"],
tags = ["automanaged"],
deps = [
"//federation/apis/federation:go_default_library",
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/deletionhelper:go_default_library",
"//federation/pkg/federation-controller/util/eventsink:go_default_library",
"//federation/pkg/federation-controller/util/planner:go_default_library",
"//federation/pkg/federation-controller/util/podanalyzer:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/errors:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/record:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/conversion:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/util/flowcontrol:go_default_library",
"//pkg/util/wait:go_default_library",
"//pkg/util/workqueue:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = ["replicasetcontroller_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5/fake:go_default_library",
"//federation/pkg/federation-controller/util/test:go_default_library",
"//pkg/api/meta:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/clientset_generated/release_1_5/fake:go_default_library",
"//pkg/client/testing/core:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/stretchr/testify/assert",
],
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,193 @@
/*
Copyright 2016 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 replicaset
import (
"flag"
"fmt"
"testing"
"time"
fedv1 "k8s.io/kubernetes/federation/apis/federation/v1beta1"
fedclientfake "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5/fake"
"k8s.io/kubernetes/federation/pkg/federation-controller/util/test"
"k8s.io/kubernetes/pkg/api/meta"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
extensionsv1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
kubeclientfake "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5/fake"
"k8s.io/kubernetes/pkg/client/testing/core"
"k8s.io/kubernetes/pkg/watch"
"github.com/stretchr/testify/assert"
)
func TestParseFederationReplicaSetReference(t *testing.T) {
successPrefs := []string{
`{"rebalance": true,
"clusters": {
"k8s-1": {"minReplicas": 10, "maxReplicas": 20, "weight": 2},
"*": {"weight": 1}
}}`,
}
failedPrefes := []string{
`{`, // bad json
}
rs := newReplicaSetWithReplicas("rs-1", 100)
accessor, _ := meta.Accessor(rs)
anno := accessor.GetAnnotations()
if anno == nil {
anno = make(map[string]string)
accessor.SetAnnotations(anno)
}
for _, prefString := range successPrefs {
anno[FedReplicaSetPreferencesAnnotation] = prefString
pref, err := parseFederationReplicaSetReference(rs)
assert.NotNil(t, pref)
assert.Nil(t, err)
}
for _, prefString := range failedPrefes {
anno[FedReplicaSetPreferencesAnnotation] = prefString
pref, err := parseFederationReplicaSetReference(rs)
assert.Nil(t, pref)
assert.NotNil(t, err)
}
}
func TestReplicaSetController(t *testing.T) {
flag.Set("logtostderr", "true")
flag.Set("v", "5")
flag.Parse()
replicaSetReviewDelay = 10 * time.Millisecond
clusterAvailableDelay = 20 * time.Millisecond
clusterUnavailableDelay = 60 * time.Millisecond
allReplicaSetReviewDelay = 120 * time.Millisecond
fedclientset := fedclientfake.NewSimpleClientset()
fedrswatch := watch.NewFake()
fedclientset.PrependWatchReactor("replicasets", core.DefaultWatchReactor(fedrswatch, nil))
fedclientset.Federation().Clusters().Create(testutil.NewCluster("k8s-1", apiv1.ConditionTrue))
fedclientset.Federation().Clusters().Create(testutil.NewCluster("k8s-2", apiv1.ConditionTrue))
kube1clientset := kubeclientfake.NewSimpleClientset()
kube1rswatch := watch.NewFake()
kube1clientset.PrependWatchReactor("replicasets", core.DefaultWatchReactor(kube1rswatch, nil))
kube1Podwatch := watch.NewFake()
kube1clientset.PrependWatchReactor("pods", core.DefaultWatchReactor(kube1Podwatch, nil))
kube2clientset := kubeclientfake.NewSimpleClientset()
kube2rswatch := watch.NewFake()
kube2clientset.PrependWatchReactor("replicasets", core.DefaultWatchReactor(kube2rswatch, nil))
kube2Podwatch := watch.NewFake()
kube2clientset.PrependWatchReactor("pods", core.DefaultWatchReactor(kube2Podwatch, nil))
fedInformerClientFactory := func(cluster *fedv1.Cluster) (kubeclientset.Interface, error) {
switch cluster.Name {
case "k8s-1":
return kube1clientset, nil
case "k8s-2":
return kube2clientset, nil
default:
return nil, fmt.Errorf("Unknown cluster: %v", cluster.Name)
}
}
replicaSetController := NewReplicaSetController(fedclientset)
rsFedinformer := testutil.ToFederatedInformerForTestOnly(replicaSetController.fedReplicaSetInformer)
rsFedinformer.SetClientFactory(fedInformerClientFactory)
podFedinformer := testutil.ToFederatedInformerForTestOnly(replicaSetController.fedPodInformer)
podFedinformer.SetClientFactory(fedInformerClientFactory)
stopChan := make(chan struct{})
defer close(stopChan)
go replicaSetController.Run(1, stopChan)
rs := newReplicaSetWithReplicas("rs", 9)
rs, _ = fedclientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).Create(rs)
fedrswatch.Add(rs)
time.Sleep(1 * time.Second)
rs1, _ := kube1clientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).Get(rs.Name)
kube1rswatch.Add(rs1)
rs1.Status.Replicas = *rs1.Spec.Replicas
rs1.Status.FullyLabeledReplicas = *rs1.Spec.Replicas
rs1.Status.ReadyReplicas = *rs1.Spec.Replicas
rs1.Status.AvailableReplicas = *rs1.Spec.Replicas
rs1, _ = kube1clientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).UpdateStatus(rs1)
kube1rswatch.Modify(rs1)
rs2, _ := kube2clientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).Get(rs.Name)
kube2rswatch.Add(rs2)
rs2.Status.Replicas = *rs2.Spec.Replicas
rs2.Status.FullyLabeledReplicas = *rs2.Spec.Replicas
rs2.Status.ReadyReplicas = *rs2.Spec.Replicas
rs2.Status.AvailableReplicas = *rs2.Spec.Replicas
rs2, _ = kube2clientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).UpdateStatus(rs2)
kube2rswatch.Modify(rs2)
time.Sleep(1 * time.Second)
rs, _ = fedclientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).Get(rs.Name)
assert.Equal(t, *rs.Spec.Replicas, *rs1.Spec.Replicas+*rs2.Spec.Replicas)
assert.Equal(t, rs.Status.Replicas, rs1.Status.Replicas+rs2.Status.Replicas)
assert.Equal(t, rs.Status.FullyLabeledReplicas, rs1.Status.FullyLabeledReplicas+rs2.Status.FullyLabeledReplicas)
assert.Equal(t, rs.Status.ReadyReplicas, rs1.Status.ReadyReplicas+rs2.Status.ReadyReplicas)
assert.Equal(t, rs.Status.AvailableReplicas, rs1.Status.AvailableReplicas+rs2.Status.AvailableReplicas)
var replicas int32 = 20
rs.Spec.Replicas = &replicas
rs, _ = fedclientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).Update(rs)
fedrswatch.Modify(rs)
time.Sleep(1 * time.Second)
rs1, _ = kube1clientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).Get(rs.Name)
rs1.Status.Replicas = *rs1.Spec.Replicas
rs1.Status.FullyLabeledReplicas = *rs1.Spec.Replicas
rs1.Status.ReadyReplicas = *rs1.Spec.Replicas
rs1.Status.AvailableReplicas = *rs1.Spec.Replicas
rs1, _ = kube1clientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).UpdateStatus(rs1)
kube1rswatch.Modify(rs1)
rs2, _ = kube2clientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).Get(rs.Name)
rs2.Status.Replicas = *rs2.Spec.Replicas
rs2.Status.FullyLabeledReplicas = *rs2.Spec.Replicas
rs2.Status.ReadyReplicas = *rs2.Spec.Replicas
rs2.Status.AvailableReplicas = *rs2.Spec.Replicas
rs2, _ = kube2clientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).UpdateStatus(rs2)
kube2rswatch.Modify(rs2)
time.Sleep(1 * time.Second)
rs, _ = fedclientset.Extensions().ReplicaSets(apiv1.NamespaceDefault).Get(rs.Name)
assert.Equal(t, *rs.Spec.Replicas, *rs1.Spec.Replicas+*rs2.Spec.Replicas)
assert.Equal(t, rs.Status.Replicas, rs1.Status.Replicas+rs2.Status.Replicas)
assert.Equal(t, rs.Status.FullyLabeledReplicas, rs1.Status.FullyLabeledReplicas+rs2.Status.FullyLabeledReplicas)
assert.Equal(t, rs.Status.ReadyReplicas, rs1.Status.ReadyReplicas+rs2.Status.ReadyReplicas)
assert.Equal(t, rs.Status.AvailableReplicas, rs1.Status.AvailableReplicas+rs2.Status.AvailableReplicas)
}
func newReplicaSetWithReplicas(name string, replicas int32) *extensionsv1.ReplicaSet {
return &extensionsv1.ReplicaSet{
ObjectMeta: apiv1.ObjectMeta{
Name: name,
Namespace: apiv1.NamespaceDefault,
SelfLink: "/api/v1/namespaces/default/replicasets/name",
},
Spec: extensionsv1.ReplicaSetSpec{
Replicas: &replicas,
},
}
}

View file

@ -0,0 +1,58 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["secret_controller.go"],
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/deletionhelper:go_default_library",
"//federation/pkg/federation-controller/util/eventsink:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/errors:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/record:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/conversion:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/types:go_default_library",
"//pkg/util/flowcontrol:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = ["secret_controller_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5/fake:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/deletionhelper:go_default_library",
"//federation/pkg/federation-controller/util/test:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/clientset_generated/release_1_5/fake:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/types:go_default_library",
"//pkg/util/wait:go_default_library",
"//vendor:github.com/stretchr/testify/assert",
],
)

View file

@ -0,0 +1,436 @@
/*
Copyright 2016 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 secret
import (
"fmt"
"time"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
federationclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/federation/pkg/federation-controller/util/deletionhelper"
"k8s.io/kubernetes/federation/pkg/federation-controller/util/eventsink"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/client/cache"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
"k8s.io/kubernetes/pkg/client/record"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/conversion"
pkgruntime "k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/flowcontrol"
"k8s.io/kubernetes/pkg/watch"
"github.com/golang/glog"
)
const (
allClustersKey = "ALL_CLUSTERS"
)
type SecretController struct {
// For triggering single secret reconciliation. This is used when there is an
// add/update/delete operation on a secret in either federated API server or
// in some member of the federation.
secretDeliverer *util.DelayingDeliverer
// For triggering all secrets reconciliation. This is used when
// a new cluster becomes available.
clusterDeliverer *util.DelayingDeliverer
// Contains secrets present in members of federation.
secretFederatedInformer util.FederatedInformer
// For updating members of federation.
federatedUpdater util.FederatedUpdater
// Definitions of secrets that should be federated.
secretInformerStore cache.Store
// Informer controller for secrets that should be federated.
secretInformerController cache.ControllerInterface
// Client to federated api server.
federatedApiClient federationclientset.Interface
// Backoff manager for secrets
secretBackoff *flowcontrol.Backoff
// For events
eventRecorder record.EventRecorder
deletionHelper *deletionhelper.DeletionHelper
secretReviewDelay time.Duration
clusterAvailableDelay time.Duration
smallDelay time.Duration
updateTimeout time.Duration
}
// NewSecretController returns a new secret controller
func NewSecretController(client federationclientset.Interface) *SecretController {
broadcaster := record.NewBroadcaster()
broadcaster.StartRecordingToSink(eventsink.NewFederatedEventSink(client))
recorder := broadcaster.NewRecorder(apiv1.EventSource{Component: "federated-secrets-controller"})
secretcontroller := &SecretController{
federatedApiClient: client,
secretReviewDelay: time.Second * 10,
clusterAvailableDelay: time.Second * 20,
smallDelay: time.Second * 3,
updateTimeout: time.Second * 30,
secretBackoff: flowcontrol.NewBackOff(5*time.Second, time.Minute),
eventRecorder: recorder,
}
// Build delivereres for triggering reconciliations.
secretcontroller.secretDeliverer = util.NewDelayingDeliverer()
secretcontroller.clusterDeliverer = util.NewDelayingDeliverer()
// Start informer in federated API servers on secrets that should be federated.
secretcontroller.secretInformerStore, secretcontroller.secretInformerController = cache.NewInformer(
&cache.ListWatch{
ListFunc: func(options apiv1.ListOptions) (pkgruntime.Object, error) {
return client.Core().Secrets(apiv1.NamespaceAll).List(options)
},
WatchFunc: func(options apiv1.ListOptions) (watch.Interface, error) {
return client.Core().Secrets(apiv1.NamespaceAll).Watch(options)
},
},
&apiv1.Secret{},
controller.NoResyncPeriodFunc(),
util.NewTriggerOnAllChanges(func(obj pkgruntime.Object) { secretcontroller.deliverSecretObj(obj, 0, false) }))
// Federated informer on secrets in members of federation.
secretcontroller.secretFederatedInformer = util.NewFederatedInformer(
client,
func(cluster *federationapi.Cluster, targetClient kubeclientset.Interface) (cache.Store, cache.ControllerInterface) {
return cache.NewInformer(
&cache.ListWatch{
ListFunc: func(options apiv1.ListOptions) (pkgruntime.Object, error) {
return targetClient.Core().Secrets(apiv1.NamespaceAll).List(options)
},
WatchFunc: func(options apiv1.ListOptions) (watch.Interface, error) {
return targetClient.Core().Secrets(apiv1.NamespaceAll).Watch(options)
},
},
&apiv1.Secret{},
controller.NoResyncPeriodFunc(),
// Trigger reconciliation whenever something in federated cluster is changed. In most cases it
// would be just confirmation that some secret opration succeeded.
util.NewTriggerOnAllChanges(
func(obj pkgruntime.Object) {
secretcontroller.deliverSecretObj(obj, secretcontroller.secretReviewDelay, false)
},
))
},
&util.ClusterLifecycleHandlerFuncs{
ClusterAvailable: func(cluster *federationapi.Cluster) {
// When new cluster becomes available process all the secrets again.
secretcontroller.clusterDeliverer.DeliverAt(allClustersKey, nil, time.Now().Add(secretcontroller.clusterAvailableDelay))
},
},
)
// Federated updeater along with Create/Update/Delete operations.
secretcontroller.federatedUpdater = util.NewFederatedUpdater(secretcontroller.secretFederatedInformer,
func(client kubeclientset.Interface, obj pkgruntime.Object) error {
secret := obj.(*apiv1.Secret)
_, err := client.Core().Secrets(secret.Namespace).Create(secret)
return err
},
func(client kubeclientset.Interface, obj pkgruntime.Object) error {
secret := obj.(*apiv1.Secret)
_, err := client.Core().Secrets(secret.Namespace).Update(secret)
return err
},
func(client kubeclientset.Interface, obj pkgruntime.Object) error {
secret := obj.(*apiv1.Secret)
err := client.Core().Secrets(secret.Namespace).Delete(secret.Name, &apiv1.DeleteOptions{})
return err
})
secretcontroller.deletionHelper = deletionhelper.NewDeletionHelper(
secretcontroller.hasFinalizerFunc,
secretcontroller.removeFinalizerFunc,
secretcontroller.addFinalizerFunc,
// objNameFunc
func(obj pkgruntime.Object) string {
secret := obj.(*apiv1.Secret)
return secret.Name
},
secretcontroller.updateTimeout,
secretcontroller.eventRecorder,
secretcontroller.secretFederatedInformer,
secretcontroller.federatedUpdater,
)
return secretcontroller
}
// Returns true if the given object has the given finalizer in its ObjectMeta.
func (secretcontroller *SecretController) hasFinalizerFunc(obj pkgruntime.Object, finalizer string) bool {
secret := obj.(*apiv1.Secret)
for i := range secret.ObjectMeta.Finalizers {
if string(secret.ObjectMeta.Finalizers[i]) == finalizer {
return true
}
}
return false
}
// Removes the finalizer from the given objects ObjectMeta.
// Assumes that the given object is a secret.
func (secretcontroller *SecretController) removeFinalizerFunc(obj pkgruntime.Object, finalizer string) (pkgruntime.Object, error) {
secret := obj.(*apiv1.Secret)
newFinalizers := []string{}
hasFinalizer := false
for i := range secret.ObjectMeta.Finalizers {
if string(secret.ObjectMeta.Finalizers[i]) != finalizer {
newFinalizers = append(newFinalizers, secret.ObjectMeta.Finalizers[i])
} else {
hasFinalizer = true
}
}
if !hasFinalizer {
// Nothing to do.
return obj, nil
}
secret.ObjectMeta.Finalizers = newFinalizers
secret, err := secretcontroller.federatedApiClient.Core().Secrets(secret.Namespace).Update(secret)
if err != nil {
return nil, fmt.Errorf("failed to remove finalizer %s from secret %s: %v", finalizer, secret.Name, err)
}
return secret, nil
}
// Adds the given finalizer to the given objects ObjectMeta.
// Assumes that the given object is a secret.
func (secretcontroller *SecretController) addFinalizerFunc(obj pkgruntime.Object, finalizer string) (pkgruntime.Object, error) {
secret := obj.(*apiv1.Secret)
secret.ObjectMeta.Finalizers = append(secret.ObjectMeta.Finalizers, finalizer)
secret, err := secretcontroller.federatedApiClient.Core().Secrets(secret.Namespace).Update(secret)
if err != nil {
return nil, fmt.Errorf("failed to add finalizer %s to secret %s: %v", finalizer, secret.Name, err)
}
return secret, nil
}
func (secretcontroller *SecretController) Run(stopChan <-chan struct{}) {
go secretcontroller.secretInformerController.Run(stopChan)
secretcontroller.secretFederatedInformer.Start()
go func() {
<-stopChan
secretcontroller.secretFederatedInformer.Stop()
}()
secretcontroller.secretDeliverer.StartWithHandler(func(item *util.DelayingDelivererItem) {
secret := item.Value.(*types.NamespacedName)
secretcontroller.reconcileSecret(*secret)
})
secretcontroller.clusterDeliverer.StartWithHandler(func(_ *util.DelayingDelivererItem) {
secretcontroller.reconcileSecretsOnClusterChange()
})
util.StartBackoffGC(secretcontroller.secretBackoff, stopChan)
}
func (secretcontroller *SecretController) deliverSecretObj(obj interface{}, delay time.Duration, failed bool) {
secret := obj.(*apiv1.Secret)
secretcontroller.deliverSecret(types.NamespacedName{Namespace: secret.Namespace, Name: secret.Name}, delay, failed)
}
// Adds backoff to delay if this delivery is related to some failure. Resets backoff if there was no failure.
func (secretcontroller *SecretController) deliverSecret(secret types.NamespacedName, delay time.Duration, failed bool) {
key := secret.String()
if failed {
secretcontroller.secretBackoff.Next(key, time.Now())
delay = delay + secretcontroller.secretBackoff.Get(key)
} else {
secretcontroller.secretBackoff.Reset(key)
}
secretcontroller.secretDeliverer.DeliverAfter(key, &secret, delay)
}
// Check whether all data stores are in sync. False is returned if any of the informer/stores is not yet
// synced with the corresponding api server.
func (secretcontroller *SecretController) isSynced() bool {
if !secretcontroller.secretFederatedInformer.ClustersSynced() {
glog.V(2).Infof("Cluster list not synced")
return false
}
clusters, err := secretcontroller.secretFederatedInformer.GetReadyClusters()
if err != nil {
glog.Errorf("Failed to get ready clusters: %v", err)
return false
}
if !secretcontroller.secretFederatedInformer.GetTargetStore().ClustersSynced(clusters) {
return false
}
return true
}
// The function triggers reconciliation of all federated secrets.
func (secretcontroller *SecretController) reconcileSecretsOnClusterChange() {
if !secretcontroller.isSynced() {
secretcontroller.clusterDeliverer.DeliverAt(allClustersKey, nil, time.Now().Add(secretcontroller.clusterAvailableDelay))
}
for _, obj := range secretcontroller.secretInformerStore.List() {
secret := obj.(*apiv1.Secret)
secretcontroller.deliverSecret(types.NamespacedName{Namespace: secret.Namespace, Name: secret.Name}, secretcontroller.smallDelay, false)
}
}
func (secretcontroller *SecretController) reconcileSecret(secret types.NamespacedName) {
if !secretcontroller.isSynced() {
secretcontroller.deliverSecret(secret, secretcontroller.clusterAvailableDelay, false)
return
}
key := secret.String()
baseSecretObjFromStore, exist, err := secretcontroller.secretInformerStore.GetByKey(key)
if err != nil {
glog.Errorf("Failed to query main secret store for %v: %v", key, err)
secretcontroller.deliverSecret(secret, 0, true)
return
}
if !exist {
// Not federated secret, ignoring.
return
}
// Create a copy before modifying the obj to prevent race condition with
// other readers of obj from store.
baseSecretObj, err := conversion.NewCloner().DeepCopy(baseSecretObjFromStore)
baseSecret, ok := baseSecretObj.(*apiv1.Secret)
if err != nil || !ok {
glog.Errorf("Error in retrieving obj from store: %v, %v", ok, err)
secretcontroller.deliverSecret(secret, 0, true)
return
}
if baseSecret.DeletionTimestamp != nil {
if err := secretcontroller.delete(baseSecret); err != nil {
glog.Errorf("Failed to delete %s: %v", secret, err)
secretcontroller.eventRecorder.Eventf(baseSecret, api.EventTypeNormal, "DeleteFailed",
"Secret delete failed: %v", err)
secretcontroller.deliverSecret(secret, 0, true)
}
return
}
glog.V(3).Infof("Ensuring delete object from underlying clusters finalizer for secret: %s",
baseSecret.Name)
// Add the required finalizers before creating a secret in underlying clusters.
updatedSecretObj, err := secretcontroller.deletionHelper.EnsureFinalizers(baseSecret)
if err != nil {
glog.Errorf("Failed to ensure delete object from underlying clusters finalizer in secret %s: %v",
baseSecret.Name, err)
secretcontroller.deliverSecret(secret, 0, false)
return
}
baseSecret = updatedSecretObj.(*apiv1.Secret)
glog.V(3).Infof("Syncing secret %s in underlying clusters", baseSecret.Name)
clusters, err := secretcontroller.secretFederatedInformer.GetReadyClusters()
if err != nil {
glog.Errorf("Failed to get cluster list: %v", err)
secretcontroller.deliverSecret(secret, secretcontroller.clusterAvailableDelay, false)
return
}
operations := make([]util.FederatedOperation, 0)
for _, cluster := range clusters {
clusterSecretObj, found, err := secretcontroller.secretFederatedInformer.GetTargetStore().GetByKey(cluster.Name, key)
if err != nil {
glog.Errorf("Failed to get %s from %s: %v", key, cluster.Name, err)
secretcontroller.deliverSecret(secret, 0, true)
return
}
// The data should not be modified.
desiredSecret := &apiv1.Secret{
ObjectMeta: util.DeepCopyRelevantObjectMeta(baseSecret.ObjectMeta),
Data: baseSecret.Data,
Type: baseSecret.Type,
}
if !found {
secretcontroller.eventRecorder.Eventf(baseSecret, api.EventTypeNormal, "CreateInCluster",
"Creating secret in cluster %s", cluster.Name)
operations = append(operations, util.FederatedOperation{
Type: util.OperationTypeAdd,
Obj: desiredSecret,
ClusterName: cluster.Name,
})
} else {
clusterSecret := clusterSecretObj.(*apiv1.Secret)
// Update existing secret, if needed.
if !util.SecretEquivalent(*desiredSecret, *clusterSecret) {
secretcontroller.eventRecorder.Eventf(baseSecret, api.EventTypeNormal, "UpdateInCluster",
"Updating secret in cluster %s", cluster.Name)
operations = append(operations, util.FederatedOperation{
Type: util.OperationTypeUpdate,
Obj: desiredSecret,
ClusterName: cluster.Name,
})
}
}
}
if len(operations) == 0 {
// Everything is in order
return
}
err = secretcontroller.federatedUpdater.UpdateWithOnError(operations, secretcontroller.updateTimeout,
func(op util.FederatedOperation, operror error) {
secretcontroller.eventRecorder.Eventf(baseSecret, api.EventTypeNormal, "UpdateInClusterFailed",
"Secret update in cluster %s failed: %v", op.ClusterName, operror)
})
if err != nil {
glog.Errorf("Failed to execute updates for %s: %v", key, err)
secretcontroller.deliverSecret(secret, 0, true)
return
}
// Evertyhing is in order but lets be double sure
secretcontroller.deliverSecret(secret, secretcontroller.secretReviewDelay, false)
}
// delete deletes the given secret or returns error if the deletion was not complete.
func (secretcontroller *SecretController) delete(secret *apiv1.Secret) error {
glog.V(3).Infof("Handling deletion of secret: %v", *secret)
_, err := secretcontroller.deletionHelper.HandleObjectInUnderlyingClusters(secret)
if err != nil {
return err
}
err = secretcontroller.federatedApiClient.Core().Secrets(secret.Namespace).Delete(secret.Name, nil)
if err != nil {
// Its all good if the error is not found error. That means it is deleted already and we do not have to do anything.
// This is expected when we are processing an update as a result of secret finalizer deletion.
// The process that deleted the last finalizer is also going to delete the secret and we do not have to do anything.
if !errors.IsNotFound(err) {
return fmt.Errorf("failed to delete secret: %v", err)
}
}
return nil
}

View file

@ -0,0 +1,196 @@
/*
Copyright 2016 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 secret
import (
"fmt"
"reflect"
"testing"
"time"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
fakefedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5/fake"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/federation/pkg/federation-controller/util/deletionhelper"
. "k8s.io/kubernetes/federation/pkg/federation-controller/util/test"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
fakekubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5/fake"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/wait"
"github.com/stretchr/testify/assert"
)
func TestSecretController(t *testing.T) {
cluster1 := NewCluster("cluster1", apiv1.ConditionTrue)
cluster2 := NewCluster("cluster2", apiv1.ConditionTrue)
fakeClient := &fakefedclientset.Clientset{}
RegisterFakeList("clusters", &fakeClient.Fake, &federationapi.ClusterList{Items: []federationapi.Cluster{*cluster1}})
RegisterFakeList("secrets", &fakeClient.Fake, &apiv1.SecretList{Items: []apiv1.Secret{}})
secretWatch := RegisterFakeWatch("secrets", &fakeClient.Fake)
secretUpdateChan := RegisterFakeCopyOnUpdate("secrets", &fakeClient.Fake, secretWatch)
clusterWatch := RegisterFakeWatch("clusters", &fakeClient.Fake)
cluster1Client := &fakekubeclientset.Clientset{}
cluster1Watch := RegisterFakeWatch("secrets", &cluster1Client.Fake)
RegisterFakeList("secrets", &cluster1Client.Fake, &apiv1.SecretList{Items: []apiv1.Secret{}})
cluster1CreateChan := RegisterFakeCopyOnCreate("secrets", &cluster1Client.Fake, cluster1Watch)
// cluster1UpdateChan := RegisterFakeCopyOnUpdate("secrets", &cluster1Client.Fake, cluster1Watch)
cluster2Client := &fakekubeclientset.Clientset{}
cluster2Watch := RegisterFakeWatch("secrets", &cluster2Client.Fake)
RegisterFakeList("secrets", &cluster2Client.Fake, &apiv1.SecretList{Items: []apiv1.Secret{}})
cluster2CreateChan := RegisterFakeCopyOnCreate("secrets", &cluster2Client.Fake, cluster2Watch)
secretController := NewSecretController(fakeClient)
informerClientFactory := func(cluster *federationapi.Cluster) (kubeclientset.Interface, error) {
switch cluster.Name {
case cluster1.Name:
return cluster1Client, nil
case cluster2.Name:
return cluster2Client, nil
default:
return nil, fmt.Errorf("Unknown cluster")
}
}
setClientFactory(secretController.secretFederatedInformer, informerClientFactory)
secretController.clusterAvailableDelay = time.Second
secretController.secretReviewDelay = 50 * time.Millisecond
secretController.smallDelay = 20 * time.Millisecond
secretController.updateTimeout = 5 * time.Second
stop := make(chan struct{})
secretController.Run(stop)
secret1 := apiv1.Secret{
ObjectMeta: apiv1.ObjectMeta{
Name: "test-secret",
Namespace: "ns",
SelfLink: "/api/v1/namespaces/ns/secrets/test-secret",
},
Data: map[string][]byte{
"A": []byte("ala ma kota"),
"B": []byte("quick brown fox"),
},
Type: apiv1.SecretTypeOpaque,
}
// Test add federated secret.
secretWatch.Add(&secret1)
// There should be 2 updates to add both the finalizers.
updatedSecret := GetSecretFromChan(secretUpdateChan)
assert.True(t, secretController.hasFinalizerFunc(updatedSecret, deletionhelper.FinalizerDeleteFromUnderlyingClusters))
updatedSecret = GetSecretFromChan(secretUpdateChan)
assert.True(t, secretController.hasFinalizerFunc(updatedSecret, apiv1.FinalizerOrphan))
secret1 = *updatedSecret
// Verify that the secret is created in underlying cluster1.
createdSecret := GetSecretFromChan(cluster1CreateChan)
assert.NotNil(t, createdSecret)
assert.Equal(t, secret1.Namespace, createdSecret.Namespace)
assert.Equal(t, secret1.Name, createdSecret.Name)
assert.True(t, secretsEqual(secret1, *createdSecret),
fmt.Sprintf("expected: %v, actual: %v", secret1, *createdSecret))
// Wait for the secret to appear in the informer store
err := WaitForStoreUpdate(
secretController.secretFederatedInformer.GetTargetStore(),
cluster1.Name, types.NamespacedName{Namespace: secret1.Namespace, Name: secret1.Name}.String(), wait.ForeverTestTimeout)
assert.Nil(t, err, "secret should have appeared in the informer store")
/*
// TODO: Uncomment this once we have figured out why this is flaky.
// Test update federated secret.
secret1.Annotations = map[string]string{
"A": "B",
}
secretWatch.Modify(&secret1)
updatedSecret = GetSecretFromChan(cluster1UpdateChan)
assert.NotNil(t, updatedSecret)
assert.Equal(t, secret1.Name, updatedSecret.Name)
assert.Equal(t, secret1.Namespace, updatedSecret.Namespace)
assert.True(t, secretsEqual(secret1, *updatedSecret),
fmt.Sprintf("expected: %v, actual: %v", secret1, *updatedSecret))
// Wait for the secret to be updated in the informer store.
err = WaitForSecretStoreUpdate(
secretController.secretFederatedInformer.GetTargetStore(),
cluster1.Name, types.NamespacedName{Namespace: secret1.Namespace, Name: secret1.Name}.String(),
updatedSecret, wait.ForeverTestTimeout)
assert.Nil(t, err, "secret should have been updated in the informer store")
// Test update federated secret.
secret1.Data = map[string][]byte{
"config": []byte("myconfigurationfile"),
}
secretWatch.Modify(&secret1)
updatedSecret2 := GetSecretFromChan(cluster1UpdateChan)
assert.NotNil(t, updatedSecret2)
assert.Equal(t, secret1.Name, updatedSecret2.Name)
assert.Equal(t, secret1.Namespace, updatedSecret.Namespace)
assert.True(t, secretsEqual(secret1, *updatedSecret2),
fmt.Sprintf("expected: %v, actual: %v", secret1, *updatedSecret2))
*/
// Test add cluster
clusterWatch.Add(cluster2)
createdSecret2 := GetSecretFromChan(cluster2CreateChan)
assert.NotNil(t, createdSecret2)
assert.Equal(t, secret1.Name, createdSecret2.Name)
assert.Equal(t, secret1.Namespace, createdSecret2.Namespace)
assert.True(t, secretsEqual(secret1, *createdSecret2),
fmt.Sprintf("expected: %v, actual: %v", secret1, *createdSecret2))
close(stop)
}
func setClientFactory(informer util.FederatedInformer, informerClientFactory func(*federationapi.Cluster) (kubeclientset.Interface, error)) {
testInformer := ToFederatedInformerForTestOnly(informer)
testInformer.SetClientFactory(informerClientFactory)
}
func secretsEqual(a, b apiv1.Secret) bool {
// Clear the SelfLink and ObjectMeta.Finalizers since they will be different
// in resoure in federation control plane and resource in underlying cluster.
a.SelfLink = ""
b.SelfLink = ""
a.ObjectMeta.Finalizers = []string{}
b.ObjectMeta.Finalizers = []string{}
return reflect.DeepEqual(a, b)
}
func GetSecretFromChan(c chan runtime.Object) *apiv1.Secret {
secret := GetObjectFromChan(c).(*apiv1.Secret)
return secret
}
// Wait till the store is updated with latest secret.
func WaitForSecretStoreUpdate(store util.FederatedReadOnlyStore, clusterName, key string, desiredSecret *apiv1.Secret, timeout time.Duration) error {
retryInterval := 100 * time.Millisecond
err := wait.PollImmediate(retryInterval, timeout, func() (bool, error) {
obj, found, err := store.GetByKey(clusterName, key)
if !found || err != nil {
return false, err
}
equal := secretsEqual(*obj.(*apiv1.Secret), *desiredSecret)
return equal, err
})
return err
}

View file

@ -0,0 +1,65 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = [
"cluster_helper.go",
"dns.go",
"doc.go",
"endpoint_helper.go",
"service_helper.go",
"servicecontroller.go",
],
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/cache:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//federation/pkg/dnsprovider:go_default_library",
"//federation/pkg/dnsprovider/rrstype:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//pkg/api/errors:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/record:go_default_library",
"//pkg/client/restclient:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/conversion:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/util/runtime:go_default_library",
"//pkg/util/sets:go_default_library",
"//pkg/util/wait:go_default_library",
"//pkg/util/workqueue:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = [
"dns_test.go",
"endpoint_helper_test.go",
"service_helper_test.go",
"servicecontroller_test.go",
],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/pkg/dnsprovider/providers/google/clouddns:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/util/sets:go_default_library",
],
)

View file

@ -0,0 +1,205 @@
/*
Copyright 2016 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 service
import (
"sync"
v1beta1 "k8s.io/kubernetes/federation/apis/federation/v1beta1"
v1 "k8s.io/kubernetes/pkg/api/v1"
cache "k8s.io/kubernetes/pkg/client/cache"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
"k8s.io/kubernetes/pkg/client/restclient"
pkgruntime "k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/wait"
"k8s.io/kubernetes/pkg/util/workqueue"
"k8s.io/kubernetes/pkg/watch"
"reflect"
"github.com/golang/glog"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
)
type clusterCache struct {
clientset *kubeclientset.Clientset
cluster *v1beta1.Cluster
// A store of services, populated by the serviceController
serviceStore cache.StoreToServiceLister
// Watches changes to all services
serviceController *cache.Controller
// A store of endpoint, populated by the serviceController
endpointStore cache.StoreToEndpointsLister
// Watches changes to all endpoints
endpointController *cache.Controller
// services that need to be synced
serviceQueue *workqueue.Type
// endpoints that need to be synced
endpointQueue *workqueue.Type
}
type clusterClientCache struct {
rwlock sync.Mutex // protects serviceMap
clientMap map[string]*clusterCache
}
func (cc *clusterClientCache) startClusterLW(cluster *v1beta1.Cluster, clusterName string) {
cachedClusterClient, ok := cc.clientMap[clusterName]
// only create when no existing cachedClusterClient
if ok {
if !reflect.DeepEqual(cachedClusterClient.cluster.Spec, cluster.Spec) {
//rebuild clientset when cluster spec is changed
clientset, err := newClusterClientset(cluster)
if err != nil || clientset == nil {
glog.Errorf("Failed to create corresponding restclient of kubernetes cluster: %v", err)
}
glog.V(4).Infof("Cluster spec changed, rebuild clientset for cluster %s", clusterName)
cachedClusterClient.clientset = clientset
go cachedClusterClient.serviceController.Run(wait.NeverStop)
go cachedClusterClient.endpointController.Run(wait.NeverStop)
glog.V(2).Infof("Start watching services and endpoints on cluster %s", clusterName)
} else {
// do nothing when there is no spec change
glog.V(4).Infof("Keep clientset for cluster %s", clusterName)
return
}
} else {
glog.V(4).Infof("No client cache for cluster %s, building new", clusterName)
clientset, err := newClusterClientset(cluster)
if err != nil || clientset == nil {
glog.Errorf("Failed to create corresponding restclient of kubernetes cluster: %v", err)
}
cachedClusterClient = &clusterCache{
cluster: cluster,
clientset: clientset,
serviceQueue: workqueue.New(),
endpointQueue: workqueue.New(),
}
cachedClusterClient.endpointStore.Store, cachedClusterClient.endpointController = cache.NewInformer(
&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (pkgruntime.Object, error) {
return clientset.Core().Endpoints(v1.NamespaceAll).List(options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
return clientset.Core().Endpoints(v1.NamespaceAll).Watch(options)
},
},
&v1.Endpoints{},
serviceSyncPeriod,
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
cc.enqueueEndpoint(obj, clusterName)
},
UpdateFunc: func(old, cur interface{}) {
cc.enqueueEndpoint(cur, clusterName)
},
DeleteFunc: func(obj interface{}) {
cc.enqueueEndpoint(obj, clusterName)
},
},
)
cachedClusterClient.serviceStore.Indexer, cachedClusterClient.serviceController = cache.NewIndexerInformer(
&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (pkgruntime.Object, error) {
return clientset.Core().Services(v1.NamespaceAll).List(options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
return clientset.Core().Services(v1.NamespaceAll).Watch(options)
},
},
&v1.Service{},
serviceSyncPeriod,
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
cc.enqueueService(obj, clusterName)
},
UpdateFunc: func(old, cur interface{}) {
oldService, ok := old.(*v1.Service)
if !ok {
return
}
curService, ok := cur.(*v1.Service)
if !ok {
return
}
if !reflect.DeepEqual(oldService.Status.LoadBalancer, curService.Status.LoadBalancer) {
cc.enqueueService(cur, clusterName)
}
},
DeleteFunc: func(obj interface{}) {
service, _ := obj.(*v1.Service)
cc.enqueueService(obj, clusterName)
glog.V(2).Infof("Service %s/%s deletion found and enque to service store %s", service.Namespace, service.Name, clusterName)
},
},
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
)
cc.clientMap[clusterName] = cachedClusterClient
go cachedClusterClient.serviceController.Run(wait.NeverStop)
go cachedClusterClient.endpointController.Run(wait.NeverStop)
glog.V(2).Infof("Start watching services and endpoints on cluster %s", clusterName)
}
}
//TODO: copied from cluster controller, to make this as common function in pass 2
// delFromClusterSet delete a cluster from clusterSet and
// delete the corresponding restclient from the map clusterKubeClientMap
func (cc *clusterClientCache) delFromClusterSet(obj interface{}) {
cluster, ok := obj.(*v1beta1.Cluster)
cc.rwlock.Lock()
defer cc.rwlock.Unlock()
if ok {
delete(cc.clientMap, cluster.Name)
} else {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
glog.Infof("Object contained wasn't a cluster or a deleted key: %+v", obj)
return
}
glog.Infof("Found tombstone for %v", obj)
delete(cc.clientMap, tombstone.Key)
}
}
// addToClusterSet inserts the new cluster to clusterSet and creates a corresponding
// restclient to map clusterKubeClientMap
func (cc *clusterClientCache) addToClientMap(obj interface{}) {
cc.rwlock.Lock()
defer cc.rwlock.Unlock()
cluster, ok := obj.(*v1beta1.Cluster)
if !ok {
return
}
pred := getClusterConditionPredicate()
// check status
// skip if not ready
if pred(*cluster) {
cc.startClusterLW(cluster, cluster.Name)
}
}
func newClusterClientset(c *v1beta1.Cluster) (*kubeclientset.Clientset, error) {
clusterConfig, err := util.BuildClusterConfig(c)
if clusterConfig != nil {
clientset := kubeclientset.NewForConfigOrDie(restclient.AddUserAgent(clusterConfig, UserAgentName))
return clientset, nil
}
return nil, err
}

View file

@ -0,0 +1,366 @@
/*
Copyright 2016 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 service
import (
"fmt"
"net"
"github.com/golang/glog"
"strings"
"k8s.io/kubernetes/federation/pkg/dnsprovider"
"k8s.io/kubernetes/federation/pkg/dnsprovider/rrstype"
)
const (
// minDnsTtl is the minimum safe DNS TTL value to use (in seconds). We use this as the TTL for all DNS records.
minDnsTtl = 180
)
// getHealthyEndpoints returns the hostnames and/or IP addresses of healthy endpoints for the service, at a zone, region and global level (or an error)
func (s *ServiceController) getHealthyEndpoints(clusterName string, cachedService *cachedService) (zoneEndpoints, regionEndpoints, globalEndpoints []string, err error) {
var (
zoneNames []string
regionName string
)
if zoneNames, regionName, err = s.getClusterZoneNames(clusterName); err != nil {
return nil, nil, nil, err
}
for lbClusterName, lbStatus := range cachedService.serviceStatusMap {
lbZoneNames, lbRegionName, err := s.getClusterZoneNames(lbClusterName)
if err != nil {
return nil, nil, nil, err
}
for _, ingress := range lbStatus.Ingress {
readyEndpoints, ok := cachedService.endpointMap[lbClusterName]
if !ok || readyEndpoints == 0 {
continue
}
var address string
// We should get either an IP address or a hostname - use whichever one we get
if ingress.IP != "" {
address = ingress.IP
} else if ingress.Hostname != "" {
address = ingress.Hostname
}
if len(address) <= 0 {
return nil, nil, nil, fmt.Errorf("Service %s/%s in cluster %s has neither LoadBalancerStatus.ingress.ip nor LoadBalancerStatus.ingress.hostname. Cannot use it as endpoint for federated service.",
cachedService.lastState.Name, cachedService.lastState.Namespace, clusterName)
}
for _, lbZoneName := range lbZoneNames {
for _, zoneName := range zoneNames {
if lbZoneName == zoneName {
zoneEndpoints = append(zoneEndpoints, address)
}
}
}
if lbRegionName == regionName {
regionEndpoints = append(regionEndpoints, address)
}
globalEndpoints = append(globalEndpoints, address)
}
}
return zoneEndpoints, regionEndpoints, globalEndpoints, nil
}
// getClusterZoneNames returns the name of the zones (and the region) where the specified cluster exists (e.g. zones "us-east1-c" on GCE, or "us-east-1b" on AWS)
func (s *ServiceController) getClusterZoneNames(clusterName string) (zones []string, region string, err error) {
client, ok := s.clusterCache.clientMap[clusterName]
if !ok {
return nil, "", fmt.Errorf("Cluster cache does not contain entry for cluster %s", clusterName)
}
if client.cluster == nil {
return nil, "", fmt.Errorf("Cluster cache entry for cluster %s is nil", clusterName)
}
return client.cluster.Status.Zones, client.cluster.Status.Region, nil
}
// getServiceDnsSuffix returns the DNS suffix to use when creating federated-service DNS records
func (s *ServiceController) getServiceDnsSuffix() (string, error) {
return s.serviceDnsSuffix, nil
}
// getDnsZones returns the DNS zones matching dnsZoneName and dnsZoneID (if specified)
func getDnsZones(dnsZoneName string, dnsZoneID string, dnsZonesInterface dnsprovider.Zones) ([]dnsprovider.Zone, error) {
// TODO: We need query-by-name and query-by-id functions
dnsZones, err := dnsZonesInterface.List()
if err != nil {
return nil, err
}
var matches []dnsprovider.Zone
findName := strings.TrimSuffix(dnsZoneName, ".")
for _, dnsZone := range dnsZones {
if dnsZoneID != "" {
if dnsZoneID != dnsZone.ID() {
continue
}
}
if findName != "" {
if strings.TrimSuffix(dnsZone.Name(), ".") != findName {
continue
}
}
matches = append(matches, dnsZone)
}
return matches, nil
}
// getDnsZone returns the DNS zone, as identified by dnsZoneName and dnsZoneID
// This is similar to getDnsZones, but returns an error if there are zero or multiple matching zones.
func getDnsZone(dnsZoneName string, dnsZoneID string, dnsZonesInterface dnsprovider.Zones) (dnsprovider.Zone, error) {
dnsZones, err := getDnsZones(dnsZoneName, dnsZoneID, dnsZonesInterface)
if err != nil {
return nil, err
}
if len(dnsZones) == 1 {
return dnsZones[0], nil
}
name := dnsZoneName
if dnsZoneID != "" {
name += "/" + dnsZoneID
}
if len(dnsZones) == 0 {
return nil, fmt.Errorf("DNS zone %s not found.", name)
} else {
return nil, fmt.Errorf("DNS zone %s is ambiguous (please specify zoneID).", name)
}
}
/* getRrset is a hack around the fact that dnsprovider.ResourceRecordSets interface does not yet include a Get() method, only a List() method. TODO: Fix that.
Note that if the named resource record set does not exist, but no error occurred, the returned set, and error, are both nil
*/
func getRrset(dnsName string, rrsetsInterface dnsprovider.ResourceRecordSets) (dnsprovider.ResourceRecordSet, error) {
var returnVal dnsprovider.ResourceRecordSet
rrsets, err := rrsetsInterface.List()
if err != nil {
return nil, err
}
for _, rrset := range rrsets {
if rrset.Name() == dnsName {
returnVal = rrset
break
}
}
return returnVal, nil
}
/* getResolvedEndpoints performs DNS resolution on the provided slice of endpoints (which might be DNS names or IPv4 addresses)
and returns a list of IPv4 addresses. If any of the endpoints are neither valid IPv4 addresses nor resolvable DNS names,
non-nil error is also returned (possibly along with a partially complete list of resolved endpoints.
*/
func getResolvedEndpoints(endpoints []string) ([]string, error) {
resolvedEndpoints := make([]string, 0, len(endpoints))
for _, endpoint := range endpoints {
if net.ParseIP(endpoint) == nil {
// It's not a valid IP address, so assume it's a DNS name, and try to resolve it,
// replacing its DNS name with its IP addresses in expandedEndpoints
ipAddrs, err := net.LookupHost(endpoint)
if err != nil {
return resolvedEndpoints, err
}
resolvedEndpoints = append(resolvedEndpoints, ipAddrs...)
} else {
resolvedEndpoints = append(resolvedEndpoints, endpoint)
}
}
return resolvedEndpoints, nil
}
/* ensureDnsRrsets ensures (idempotently, and with minimum mutations) that all of the DNS resource record sets for dnsName are consistent with endpoints.
if endpoints is nil or empty, a CNAME record to uplevelCname is ensured.
*/
func (s *ServiceController) ensureDnsRrsets(dnsZone dnsprovider.Zone, dnsName string, endpoints []string, uplevelCname string) error {
rrsets, supported := dnsZone.ResourceRecordSets()
if !supported {
return fmt.Errorf("Failed to ensure DNS records for %s. DNS provider does not support the ResourceRecordSets interface.", dnsName)
}
rrset, err := getRrset(dnsName, rrsets) // TODO: rrsets.Get(dnsName)
if err != nil {
return err
}
if rrset == nil {
glog.V(4).Infof("No recordsets found for DNS name %q. Need to add either A records (if we have healthy endpoints), or a CNAME record to %q", dnsName, uplevelCname)
if len(endpoints) < 1 {
glog.V(4).Infof("There are no healthy endpoint addresses at level %q, so CNAME to %q, if provided", dnsName, uplevelCname)
if uplevelCname != "" {
glog.V(4).Infof("Creating CNAME to %q for %q", uplevelCname, dnsName)
newRrset := rrsets.New(dnsName, []string{uplevelCname}, minDnsTtl, rrstype.CNAME)
glog.V(4).Infof("Adding recordset %v", newRrset)
err = rrsets.StartChangeset().Add(newRrset).Apply()
if err != nil {
return err
}
glog.V(4).Infof("Successfully created CNAME to %q for %q", uplevelCname, dnsName)
} else {
glog.V(4).Infof("We want no record for %q, and we have no record, so we're all good.", dnsName)
}
} else {
// We have valid endpoint addresses, so just add them as A records.
// But first resolve DNS names, as some cloud providers (like AWS) expose
// load balancers behind DNS names, not IP addresses.
glog.V(4).Infof("We have valid endpoint addresses %v at level %q, so add them as A records, after resolving DNS names", endpoints, dnsName)
resolvedEndpoints, err := getResolvedEndpoints(endpoints)
if err != nil {
return err // TODO: We could potentially add the ones we did get back, even if some of them failed to resolve.
}
newRrset := rrsets.New(dnsName, resolvedEndpoints, minDnsTtl, rrstype.A)
glog.V(4).Infof("Adding recordset %v", newRrset)
err = rrsets.StartChangeset().Add(newRrset).Apply()
if err != nil {
return err
}
glog.V(4).Infof("Successfully added recordset %v", newRrset)
}
} else {
// the rrset already exists, so make it right.
glog.V(4).Infof("Recordset %v already exists. Ensuring that it is correct.", rrset)
if len(endpoints) < 1 {
// Need an appropriate CNAME record. Check that we have it.
newRrset := rrsets.New(dnsName, []string{uplevelCname}, minDnsTtl, rrstype.CNAME)
glog.V(4).Infof("No healthy endpoints for %s. Have recordset %v. Need recordset %v", dnsName, rrset, newRrset)
if dnsprovider.ResourceRecordSetsEquivalent(rrset, newRrset) {
// The existing rrset is equivalent to the required one - our work is done here
glog.V(4).Infof("Existing recordset %v is equivalent to needed recordset %v, our work is done here.", rrset, newRrset)
return nil
} else {
// Need to replace the existing one with a better one (or just remove it if we have no healthy endpoints).
glog.V(4).Infof("Existing recordset %v not equivalent to needed recordset %v removing existing and adding needed.", rrset, newRrset)
changeSet := rrsets.StartChangeset()
changeSet.Remove(rrset)
if uplevelCname != "" {
changeSet.Add(newRrset)
if err := changeSet.Apply(); err != nil {
return err
}
glog.V(4).Infof("Successfully replaced needed recordset %v -> %v", rrset, newRrset)
} else {
if err := changeSet.Apply(); err != nil {
return err
}
glog.V(4).Infof("Successfully removed existing recordset %v", rrset)
glog.V(4).Infof("Uplevel CNAME is empty string. Not adding recordset %v", newRrset)
}
}
} else {
// We have an rrset in DNS, possibly with some missing addresses and some unwanted addresses.
// And we have healthy endpoints. Just replace what's there with the healthy endpoints, if it's not already correct.
glog.V(4).Infof("%s: Healthy endpoints %v exist. Recordset %v exists. Reconciling.", dnsName, endpoints, rrset)
resolvedEndpoints, err := getResolvedEndpoints(endpoints)
if err != nil { // Some invalid addresses or otherwise unresolvable DNS names.
return err // TODO: We could potentially add the ones we did get back, even if some of them failed to resolve.
}
newRrset := rrsets.New(dnsName, resolvedEndpoints, minDnsTtl, rrstype.A)
glog.V(4).Infof("Have recordset %v. Need recordset %v", rrset, newRrset)
if dnsprovider.ResourceRecordSetsEquivalent(rrset, newRrset) {
glog.V(4).Infof("Existing recordset %v is equivalent to needed recordset %v, our work is done here.", rrset, newRrset)
// TODO: We could be more thorough about checking for equivalence to avoid unnecessary updates, but in the
// worst case we'll just replace what's there with an equivalent, if not exactly identical record set.
return nil
} else {
// Need to replace the existing one with a better one
glog.V(4).Infof("Existing recordset %v is not equivalent to needed recordset %v, removing existing and adding needed.", rrset, newRrset)
if err = rrsets.StartChangeset().Remove(rrset).Add(newRrset).Apply(); err != nil {
return err
}
glog.V(4).Infof("Successfully replaced recordset %v -> %v", rrset, newRrset)
}
}
}
return nil
}
/* ensureDnsRecords ensures (idempotently, and with minimum mutations) that all of the DNS records for a service in a given cluster are correct,
given the current state of that service in that cluster. This should be called every time the state of a service might have changed
(either w.r.t. it's loadbalancer address, or if the number of healthy backend endpoints for that service transitioned from zero to non-zero
(or vice verse). Only shards of the service which have both a loadbalancer ingress IP address or hostname AND at least one healthy backend endpoint
are included in DNS records for that service (at all of zone, region and global levels). All other addresses are removed. Also, if no shards exist
in the zone or region of the cluster, a CNAME reference to the next higher level is ensured to exist. */
func (s *ServiceController) ensureDnsRecords(clusterName string, cachedService *cachedService) error {
// Quinton: Pseudocode....
// See https://github.com/kubernetes/kubernetes/pull/25107#issuecomment-218026648
// For each service we need the following DNS names:
// mysvc.myns.myfed.svc.z1.r1.mydomain.com (for zone z1 in region r1)
// - an A record to IP address of specific shard in that zone (if that shard exists and has healthy endpoints)
// - OR a CNAME record to the next level up, i.e. mysvc.myns.myfed.svc.r1.mydomain.com (if a healthy shard does not exist in zone z1)
// mysvc.myns.myfed.svc.r1.federation
// - a set of A records to IP addresses of all healthy shards in region r1, if one or more of these exist
// - OR a CNAME record to the next level up, i.e. mysvc.myns.myfed.svc.mydomain.com (if no healthy shards exist in region r1)
// mysvc.myns.myfed.svc.federation
// - a set of A records to IP addresses of all healthy shards in all regions, if one or more of these exist.
// - no record (NXRECORD response) if no healthy shards exist in any regions)
//
// For each cached service, cachedService.lastState tracks the current known state of the service, while cachedService.appliedState contains
// the state of the service when we last successfully sync'd it's DNS records.
// So this time around we only need to patch that (add new records, remove deleted records, and update changed records.
//
if s == nil {
return fmt.Errorf("nil ServiceController passed to ServiceController.ensureDnsRecords(clusterName: %s, cachedService: %v)", clusterName, cachedService)
}
if s.dns == nil {
return nil
}
if cachedService == nil {
return fmt.Errorf("nil cachedService passed to ServiceController.ensureDnsRecords(clusterName: %s, cachedService: %v)", clusterName, cachedService)
}
serviceName := cachedService.lastState.Name
namespaceName := cachedService.lastState.Namespace
zoneNames, regionName, err := s.getClusterZoneNames(clusterName)
if err != nil {
return err
}
if zoneNames == nil {
return fmt.Errorf("failed to get cluster zone names")
}
serviceDnsSuffix, err := s.getServiceDnsSuffix()
if err != nil {
return err
}
zoneEndpoints, regionEndpoints, globalEndpoints, err := s.getHealthyEndpoints(clusterName, cachedService)
if err != nil {
return err
}
commonPrefix := serviceName + "." + namespaceName + "." + s.federationName + ".svc"
// dnsNames is the path up the DNS search tree, starting at the leaf
dnsNames := []string{
commonPrefix + "." + zoneNames[0] + "." + regionName + "." + serviceDnsSuffix, // zone level - TODO might need other zone names for multi-zone clusters
commonPrefix + "." + regionName + "." + serviceDnsSuffix, // region level, one up from zone level
commonPrefix + "." + serviceDnsSuffix, // global level, one up from region level
"", // nowhere to go up from global level
}
endpoints := [][]string{zoneEndpoints, regionEndpoints, globalEndpoints}
dnsZone, err := getDnsZone(s.zoneName, s.zoneID, s.dnsZones)
if err != nil {
return err
}
for i, endpoint := range endpoints {
if err = s.ensureDnsRrsets(dnsZone, dnsNames[i], endpoint, dnsNames[i+1]); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,171 @@
/*
Copyright 2016 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 service
import (
"sync"
"testing"
"fmt"
"reflect"
"sort"
"k8s.io/kubernetes/federation/apis/federation/v1beta1"
"k8s.io/kubernetes/federation/pkg/dnsprovider/providers/google/clouddns" // Only for unit testing purposes.
"k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/util/sets"
)
func TestServiceController_ensureDnsRecords(t *testing.T) {
tests := []struct {
name string
service v1.Service
expected []string
serviceStatus v1.LoadBalancerStatus
}{
{
name: "withip",
service: v1.Service{
ObjectMeta: v1.ObjectMeta{
Name: "servicename",
Namespace: "servicenamespace",
},
},
serviceStatus: buildServiceStatus([][]string{{"198.51.100.1", ""}}),
expected: []string{
"example.com:servicename.servicenamespace.myfederation.svc.federation.example.com:A:180:[198.51.100.1]",
"example.com:servicename.servicenamespace.myfederation.svc.fooregion.federation.example.com:A:180:[198.51.100.1]",
"example.com:servicename.servicenamespace.myfederation.svc.foozone.fooregion.federation.example.com:A:180:[198.51.100.1]",
},
},
/*
TODO: getResolvedEndpoints preforms DNS lookup.
Mock and maybe look at error handling when some endpoints resolve, but also caching?
{
name: "withname",
service: v1.Service{
ObjectMeta: v1.ObjectMeta{
Name: "servicename",
Namespace: "servicenamespace",
},
},
serviceStatus: buildServiceStatus([][]string{{"", "randomstring.amazonelb.example.com"}}),
expected: []string{
"example.com:servicename.servicenamespace.myfederation.svc.federation.example.com:A:180:[198.51.100.1]",
"example.com:servicename.servicenamespace.myfederation.svc.fooregion.federation.example.com:A:180:[198.51.100.1]",
"example.com:servicename.servicenamespace.myfederation.svc.foozone.fooregion.federation.example.com:A:180:[198.51.100.1]",
},
},
*/
{
name: "noendpoints",
service: v1.Service{
ObjectMeta: v1.ObjectMeta{
Name: "servicename",
Namespace: "servicenamespace",
},
},
expected: []string{
"example.com:servicename.servicenamespace.myfederation.svc.fooregion.federation.example.com:CNAME:180:[servicename.servicenamespace.myfederation.svc.federation.example.com]",
"example.com:servicename.servicenamespace.myfederation.svc.foozone.fooregion.federation.example.com:CNAME:180:[servicename.servicenamespace.myfederation.svc.fooregion.federation.example.com]",
},
},
}
for _, test := range tests {
fakedns, _ := clouddns.NewFakeInterface()
fakednsZones, ok := fakedns.Zones()
if !ok {
t.Error("Unable to fetch zones")
}
serviceController := ServiceController{
dns: fakedns,
dnsZones: fakednsZones,
serviceDnsSuffix: "federation.example.com",
zoneName: "example.com",
federationName: "myfederation",
serviceCache: &serviceCache{fedServiceMap: make(map[string]*cachedService)},
clusterCache: &clusterClientCache{
rwlock: sync.Mutex{},
clientMap: make(map[string]*clusterCache),
},
knownClusterSet: make(sets.String),
}
clusterName := "testcluster"
serviceController.clusterCache.clientMap[clusterName] = &clusterCache{
cluster: &v1beta1.Cluster{
Status: v1beta1.ClusterStatus{
Zones: []string{"foozone"},
Region: "fooregion",
},
},
}
cachedService := &cachedService{
lastState: &test.service,
endpointMap: make(map[string]int),
serviceStatusMap: make(map[string]v1.LoadBalancerStatus),
}
cachedService.endpointMap[clusterName] = 1
if !reflect.DeepEqual(&test.serviceStatus, &v1.LoadBalancerStatus{}) {
cachedService.serviceStatusMap[clusterName] = test.serviceStatus
}
err := serviceController.ensureDnsRecords(clusterName, cachedService)
if err != nil {
t.Errorf("Test failed for %s, unexpected error %v", test.name, err)
}
zones, err := fakednsZones.List()
if err != nil {
t.Errorf("error querying zones: %v", err)
}
// Dump every record to a testable-by-string-comparison form
var records []string
for _, z := range zones {
zoneName := z.Name()
rrs, ok := z.ResourceRecordSets()
if !ok {
t.Errorf("cannot get rrs for zone %q", zoneName)
}
rrList, err := rrs.List()
if err != nil {
t.Errorf("error querying rr for zone %q: %v", zoneName, err)
}
for _, rr := range rrList {
rrdatas := rr.Rrdatas()
// Put in consistent (testable-by-string-comparison) order
sort.Strings(rrdatas)
records = append(records, fmt.Sprintf("%s:%s:%s:%d:%s", zoneName, rr.Name(), rr.Type(), rr.Ttl(), rrdatas))
}
}
// Ignore order of records
sort.Strings(records)
sort.Strings(test.expected)
if !reflect.DeepEqual(records, test.expected) {
t.Errorf("Test %q failed. Actual=%v, Expected=%v", test.name, records, test.expected)
}
}
}

View file

@ -0,0 +1,19 @@
/*
Copyright 2016 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 service contains code for syncing Kubernetes services,
// and cloud DNS servers with the federated service registry.
package service // import "k8s.io/kubernetes/federation/pkg/federation-controller/service"

View file

@ -0,0 +1,206 @@
/*
Copyright 2016 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 service
import (
"fmt"
"time"
fedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5"
v1 "k8s.io/kubernetes/pkg/api/v1"
cache "k8s.io/kubernetes/pkg/client/cache"
"k8s.io/kubernetes/pkg/controller"
"github.com/golang/glog"
)
// worker runs a worker thread that just dequeues items, processes them, and marks them done.
// It enforces that the syncHandler is never invoked concurrently with the same key.
func (sc *ServiceController) clusterEndpointWorker() {
// process all pending events in endpointWorkerDoneChan
ForLoop:
for {
select {
case clusterName := <-sc.endpointWorkerDoneChan:
sc.endpointWorkerMap[clusterName] = false
default:
// non-blocking, comes here if all existing events are processed
break ForLoop
}
}
for clusterName, cache := range sc.clusterCache.clientMap {
workerExist, found := sc.endpointWorkerMap[clusterName]
if found && workerExist {
continue
}
// create a worker only if the previous worker has finished and gone out of scope
go func(cache *clusterCache, clusterName string) {
fedClient := sc.federationClient
for {
func() {
key, quit := cache.endpointQueue.Get()
// update endpoint cache
if quit {
// send signal that current worker has finished tasks and is going out of scope
sc.endpointWorkerDoneChan <- clusterName
return
}
defer cache.endpointQueue.Done(key)
err := sc.clusterCache.syncEndpoint(key.(string), clusterName, cache, sc.serviceCache, fedClient, sc)
if err != nil {
glog.V(2).Infof("Failed to sync endpoint: %+v", err)
}
}()
}
}(cache, clusterName)
sc.endpointWorkerMap[clusterName] = true
}
}
// Whenever there is change on endpoint, the federation service should be updated
// key is the namespaced name of endpoint
func (cc *clusterClientCache) syncEndpoint(key, clusterName string, clusterCache *clusterCache, serviceCache *serviceCache, fedClient fedclientset.Interface, serviceController *ServiceController) error {
cachedService, ok := serviceCache.get(key)
if !ok {
// here we filtered all non-federation services
return nil
}
endpointInterface, exists, err := clusterCache.endpointStore.GetByKey(key)
if err != nil {
glog.Errorf("Did not successfully get %v from store: %v, will retry later", key, err)
clusterCache.endpointQueue.Add(key)
return err
}
if exists {
endpoint, ok := endpointInterface.(*v1.Endpoints)
if ok {
glog.V(4).Infof("Found endpoint for federation service %s/%s from cluster %s", endpoint.Namespace, endpoint.Name, clusterName)
err = cc.processEndpointUpdate(cachedService, endpoint, clusterName, serviceController)
} else {
_, ok := endpointInterface.(cache.DeletedFinalStateUnknown)
if !ok {
return fmt.Errorf("Object contained wasn't a service or a deleted key: %+v", endpointInterface)
}
glog.Infof("Found tombstone for %v", key)
err = cc.processEndpointDeletion(cachedService, clusterName, serviceController)
}
} else {
// service absence in store means watcher caught the deletion, ensure LB info is cleaned
glog.Infof("Can not get endpoint %v for cluster %s from endpointStore", key, clusterName)
err = cc.processEndpointDeletion(cachedService, clusterName, serviceController)
}
if err != nil {
glog.Errorf("Failed to sync service: %+v, put back to service queue", err)
clusterCache.endpointQueue.Add(key)
}
cachedService.resetDNSUpdateDelay()
return nil
}
func (cc *clusterClientCache) processEndpointDeletion(cachedService *cachedService, clusterName string, serviceController *ServiceController) error {
glog.V(4).Infof("Processing endpoint deletion for %s/%s, cluster %s", cachedService.lastState.Namespace, cachedService.lastState.Name, clusterName)
var err error
cachedService.rwlock.Lock()
defer cachedService.rwlock.Unlock()
_, ok := cachedService.endpointMap[clusterName]
// TODO remove ok checking? if service controller is restarted, then endpointMap for the cluster does not exist
// need to query dns info from dnsprovider and make sure of if deletion is needed
if ok {
// endpoints lost, clean dns record
glog.V(4).Infof("Cached endpoint was found for %s/%s, cluster %s, removing", cachedService.lastState.Namespace, cachedService.lastState.Name, clusterName)
delete(cachedService.endpointMap, clusterName)
for i := 0; i < clientRetryCount; i++ {
err := serviceController.ensureDnsRecords(clusterName, cachedService)
if err == nil {
return nil
}
glog.V(4).Infof("Error ensuring DNS Records: %v", err)
time.Sleep(cachedService.nextDNSUpdateDelay())
}
}
return err
}
// Update dns info when endpoint update event received
// We do not care about the endpoint info, what we need to make sure here is len(endpoints.subsets)>0
func (cc *clusterClientCache) processEndpointUpdate(cachedService *cachedService, endpoint *v1.Endpoints, clusterName string, serviceController *ServiceController) error {
glog.V(4).Infof("Processing endpoint update for %s/%s, cluster %s", endpoint.Namespace, endpoint.Name, clusterName)
var err error
cachedService.rwlock.Lock()
var reachable bool
defer cachedService.rwlock.Unlock()
_, ok := cachedService.endpointMap[clusterName]
if !ok {
for _, subset := range endpoint.Subsets {
if len(subset.Addresses) > 0 {
reachable = true
break
}
}
if reachable {
// first time get endpoints, update dns record
glog.V(4).Infof("Reachable endpoint was found for %s/%s, cluster %s, building endpointMap", endpoint.Namespace, endpoint.Name, clusterName)
cachedService.endpointMap[clusterName] = 1
for i := 0; i < clientRetryCount; i++ {
err := serviceController.ensureDnsRecords(clusterName, cachedService)
if err == nil {
return nil
}
glog.V(4).Infof("Error ensuring DNS Records: %v", err)
time.Sleep(cachedService.nextDNSUpdateDelay())
}
return err
}
} else {
for _, subset := range endpoint.Subsets {
if len(subset.Addresses) > 0 {
reachable = true
break
}
}
if !reachable {
// first time get endpoints, update dns record
glog.V(4).Infof("Reachable endpoint was lost for %s/%s, cluster %s, deleting endpointMap", endpoint.Namespace, endpoint.Name, clusterName)
delete(cachedService.endpointMap, clusterName)
for i := 0; i < clientRetryCount; i++ {
err := serviceController.ensureDnsRecords(clusterName, cachedService)
if err == nil {
return nil
}
glog.V(4).Infof("Error ensuring DNS Records: %v", err)
time.Sleep(cachedService.nextDNSUpdateDelay())
}
return err
}
}
return nil
}
// obj could be an *api.Endpoints, or a DeletionFinalStateUnknown marker item.
func (cc *clusterClientCache) enqueueEndpoint(obj interface{}, clusterName string) {
key, err := controller.KeyFunc(obj)
if err != nil {
glog.Errorf("Couldn't get key for object %+v: %v", obj, err)
return
}
_, ok := cc.clientMap[clusterName]
if ok {
cc.clientMap[clusterName].endpointQueue.Add(key)
}
}

Some files were not shown because too many files have changed in this diff Show more