blob: 77f5290d411f169e2d5340a07dceb8f7a0c6e306 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 cloudstack
import (
"context"
"errors"
"strings"
"testing"
"github.com/apache/cloudstack-go/v2/cloudstack"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
restclient "k8s.io/client-go/rest"
cloudprovider "k8s.io/cloud-provider"
)
type fakeClientBuilder struct {
client kubernetes.Interface
err error
}
func (b *fakeClientBuilder) Config(string) (*restclient.Config, error) { return nil, b.err }
func (b *fakeClientBuilder) ConfigOrDie(string) *restclient.Config { return nil }
func (b *fakeClientBuilder) Client(string) (kubernetes.Interface, error) {
return b.client, b.err
}
func (b *fakeClientBuilder) ClientOrDie(string) kubernetes.Interface { return b.client }
var _ cloudprovider.ControllerClientBuilder = &fakeClientBuilder{}
type zoneLookup struct {
name string
call func(cs *CSCloud) (cloudprovider.Zone, error)
// expect registers the one CloudStack lookup the call makes.
expect func(e *vmLookupEnv, t *testing.T, result *cloudstack.VirtualMachine, count int, err error)
// notFound is the text of the error for an instance CloudStack does not know.
notFound string
}
var zoneLookups = []zoneLookup{
{
name: "GetZoneByNodeName",
call: func(cs *CSCloud) (cloudprovider.Zone, error) {
return cs.GetZoneByNodeName(context.TODO(), "node-1")
},
expect: func(e *vmLookupEnv, t *testing.T, result *cloudstack.VirtualMachine, count int, err error) {
e.expectByName(t, "node-1", result, count, err)
},
notFound: "could not find node: node-1",
},
{
name: "GetZoneByProviderID",
call: func(cs *CSCloud) (cloudprovider.Zone, error) {
return cs.GetZoneByProviderID(context.TODO(), "external-cloudstack://vm-1")
},
expect: func(e *vmLookupEnv, t *testing.T, result *cloudstack.VirtualMachine, count int, err error) {
e.expectByID(t, "vm-1", result, count, err)
},
notFound: "could not find node by ID: external-cloudstack://vm-1",
},
}
// TestZoneLookups checks that a zone is resolved from the instance's zone name in the configured
// project, that the region is the configured one or else the zone name, and that failures read
// differently for a missing instance and an API error.
func TestZoneLookups(t *testing.T) {
for _, lookup := range zoneLookups {
t.Run(lookup.name+"/region falls back to the zone", func(t *testing.T) {
env := newVMLookupEnv(t, "")
lookup.expect(env, t, testInstance(), 1, nil)
zone, err := lookup.call(env.cs)
if err != nil || zone.FailureDomain != "zone-a" || zone.Region != "zone-a" {
t.Errorf("%s() = %+v, %v, want zone-a in region zone-a", lookup.name, zone, err)
}
})
t.Run(lookup.name+"/configured region in a project", func(t *testing.T) {
env := newVMLookupEnv(t, testProjectID)
env.cs.region = "region-1"
lookup.expect(env, t, testInstance(), 1, nil)
zone, err := lookup.call(env.cs)
if err != nil || zone.FailureDomain != "zone-a" || zone.Region != "region-1" {
t.Errorf("%s() = %+v, %v, want zone-a in region region-1", lookup.name, zone, err)
}
})
t.Run(lookup.name+"/instance not found", func(t *testing.T) {
env := newVMLookupEnv(t, "")
lookup.expect(env, t, nil, 0, errors.New("no match"))
_, err := lookup.call(env.cs)
if err == nil || err.Error() != lookup.notFound {
t.Errorf("%s() error = %v, want %q", lookup.name, err, lookup.notFound)
}
})
t.Run(lookup.name+"/api error", func(t *testing.T) {
env := newVMLookupEnv(t, "")
lookup.expect(env, t, nil, 1, errors.New("boom"))
_, err := lookup.call(env.cs)
if err == nil || err.Error() != "error retrieving zone: boom" {
t.Errorf("%s() error = %v, want %q", lookup.name, err, "error retrieving zone: boom")
}
})
}
}
func podNamed(name, namespace, nodeName string) *corev1.Pod {
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
Spec: corev1.PodSpec{NodeName: nodeName},
}
}
func setPodEnvironment(t *testing.T, podName, namespace string) {
t.Helper()
t.Setenv("HOSTNAME", podName)
t.Setenv("POD_NAMESPACE", namespace)
}
// TestGetNodeNameFromPod checks that the node is read from the CCM's own pod and that every way
// that can fail is reported.
func TestGetNodeNameFromPod(t *testing.T) {
setPodEnvironment(t, "ccm-pod", "ccm-ns")
tests := []struct {
name string
builder *fakeClientBuilder
want string
errPrefix string
}{
{
name: "no client builder",
errPrefix: "clientBuilder not initialized",
},
{
name: "client cannot be built",
builder: &fakeClientBuilder{err: errors.New("no config")},
errPrefix: "failed to get Kubernetes client: no config",
},
{
name: "pod does not exist",
builder: &fakeClientBuilder{client: fake.NewSimpleClientset()},
errPrefix: "failed to get pod ccm-ns/ccm-pod from Kubernetes API",
},
{
name: "pod not scheduled yet",
builder: &fakeClientBuilder{client: fake.NewSimpleClientset(podNamed("ccm-pod", "ccm-ns", ""))},
errPrefix: "pod ccm-ns/ccm-pod does not have a nodeName assigned yet",
},
{
name: "pod scheduled",
builder: &fakeClientBuilder{client: fake.NewSimpleClientset(podNamed("ccm-pod", "ccm-ns", "node-1"))},
want: "node-1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cs := &CSCloud{}
if tt.builder != nil {
cs.clientBuilder = tt.builder
}
got, err := cs.getNodeNameFromPod(context.TODO())
if tt.errPrefix != "" {
if err == nil || !strings.HasPrefix(err.Error(), tt.errPrefix) {
t.Errorf("getNodeNameFromPod() error = %v, want prefix %q", err, tt.errPrefix)
}
return
}
if err != nil || got != tt.want {
t.Errorf("getNodeNameFromPod() = %q, %v, want %q", got, err, tt.want)
}
})
}
}
// TestGetZone checks that the zone is looked up once through the pod's node and then served from the cache.
func TestGetZone(t *testing.T) {
t.Run("cached zone makes no lookup", func(t *testing.T) {
env := newVMLookupEnv(t, "")
env.cs.zone = "zone-a"
env.cs.region = "region-1"
zone, err := env.cs.GetZone(context.TODO())
if err != nil || zone.FailureDomain != "zone-a" || zone.Region != "region-1" {
t.Errorf("GetZone() = %+v, %v, want zone-a in region region-1", zone, err)
}
})
t.Run("zone of the pod's node is looked up once", func(t *testing.T) {
setPodEnvironment(t, "ccm-pod", "ccm-ns")
env := newVMLookupEnv(t, testProjectID)
env.cs.clientBuilder = &fakeClientBuilder{client: fake.NewSimpleClientset(podNamed("ccm-pod", "ccm-ns", "node-1"))}
env.expectByName(t, "node-1", testInstance(), 1, nil).Times(1)
for i := 0; i < 2; i++ {
zone, err := env.cs.GetZone(context.TODO())
if err != nil || zone.FailureDomain != "zone-a" || zone.Region != "zone-a" {
t.Fatalf("GetZone() call %d = %+v, %v, want zone-a in region zone-a", i+1, zone, err)
}
}
})
failures := []struct {
name string
builder cloudprovider.ControllerClientBuilder
result *cloudstack.VirtualMachine
count int
err error
errPrefix string
}{
{
name: "node of the pod unknown",
errPrefix: "failed to get node name for retrieving the zone",
},
{
name: "instance not found",
builder: &fakeClientBuilder{client: fake.NewSimpleClientset(podNamed("ccm-pod", "ccm-ns", "node-1"))},
count: 0,
err: errors.New("no match"),
errPrefix: "could not find CloudStack instance with name node-1 for retrieving the zone",
},
{
name: "api error",
builder: &fakeClientBuilder{client: fake.NewSimpleClientset(podNamed("ccm-pod", "ccm-ns", "node-1"))},
count: 1,
err: errors.New("boom"),
errPrefix: "error getting instance for retrieving the zone",
},
}
for _, tt := range failures {
t.Run(tt.name, func(t *testing.T) {
setPodEnvironment(t, "ccm-pod", "ccm-ns")
env := newVMLookupEnv(t, "")
env.cs.clientBuilder = tt.builder
if tt.err != nil {
env.expectByName(t, "node-1", tt.result, tt.count, tt.err)
}
_, err := env.cs.GetZone(context.TODO())
if err == nil || !strings.HasPrefix(err.Error(), tt.errPrefix) {
t.Errorf("GetZone() error = %v, want prefix %q", err, tt.errPrefix)
}
})
}
}