clusterlib: fake boskos client for better test coverage (#649)

* Fake boskos

* feedback updates
This commit is contained in:
chaodaiG 2019-09-12 11:36:32 -07:00 committed by Knative Prow Robot
parent 343f1649fe
commit 2e2ab7a878
5 changed files with 143 additions and 29 deletions

View File

@ -37,6 +37,15 @@ var (
defaultWaitDuration = time.Minute * 20 defaultWaitDuration = time.Minute * 20
) )
type Operation interface {
AcquireGKEProject(*string) (*boskoscommon.Resource, error)
ReleaseGKEProject(*string, string) error
}
type Client struct {
*boskosclient.Client
}
func newClient(host *string) *boskosclient.Client { func newClient(host *string) *boskosclient.Client {
if nil == host { if nil == host {
hostName := common.GetOSEnv("JOB_NAME") hostName := common.GetOSEnv("JOB_NAME")
@ -48,7 +57,7 @@ func newClient(host *string) *boskosclient.Client {
// AcquireGKEProject acquires GKE Boskos Project with "free" state, and not // AcquireGKEProject acquires GKE Boskos Project with "free" state, and not
// owned by anyone, sets its state to "busy" and assign it an owner of *host, // owned by anyone, sets its state to "busy" and assign it an owner of *host,
// which by default is env var `JOB_NAME`. // which by default is env var `JOB_NAME`.
func AcquireGKEProject(host *string) (*boskoscommon.Resource, error) { func (c *Client) AcquireGKEProject(host *string) (*boskoscommon.Resource, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultWaitDuration) ctx, cancel := context.WithTimeout(context.Background(), defaultWaitDuration)
defer cancel() defer cancel()
p, err := newClient(host).AcquireWait(ctx, GKEProjectResource, boskoscommon.Free, boskoscommon.Busy) p, err := newClient(host).AcquireWait(ctx, GKEProjectResource, boskoscommon.Free, boskoscommon.Busy)
@ -66,7 +75,7 @@ func AcquireGKEProject(host *string) (*boskoscommon.Resource, error) {
// "dirty" for Janitor picking up. // "dirty" for Janitor picking up.
// This function is very powerful, it can release Boskos resource acquired by // This function is very powerful, it can release Boskos resource acquired by
// other processes, regardless of where the other process is running. // other processes, regardless of where the other process is running.
func ReleaseGKEProject(host *string, name string) error { func (c *Client) ReleaseGKEProject(host *string, name string) error {
client := newClient(host) client := newClient(host)
if err := client.Release(name, boskoscommon.Dirty); nil != err { if err := client.Release(name, boskoscommon.Dirty); nil != err {
return fmt.Errorf("boskos failed to release GKE project '%s': %v", name, err) return fmt.Errorf("boskos failed to release GKE project '%s': %v", name, err)

View File

@ -29,8 +29,14 @@ import (
var ( var (
fakeHost = "fakehost" fakeHost = "fakehost"
fakeRes = "{\"name\": \"res\", \"type\": \"t\", \"state\": \"d\"}" fakeRes = "{\"name\": \"res\", \"type\": \"t\", \"state\": \"d\"}"
client Client
) )
func setup() {
client = Client{}
}
// create a fake server as Boskos server, must close() afterwards // create a fake server as Boskos server, must close() afterwards
func fakeServer(f func(http.ResponseWriter, *http.Request)) *httptest.Server { func fakeServer(f func(http.ResponseWriter, *http.Request)) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(f)) return httptest.NewServer(http.HandlerFunc(f))
@ -66,6 +72,7 @@ func TestAcquireGKEProject(t *testing.T) {
common.GetOSEnv = oldGetOSEnv common.GetOSEnv = oldGetOSEnv
}() }()
for _, data := range datas { for _, data := range datas {
setup()
ts := fakeServer(func(w http.ResponseWriter, r *http.Request) { ts := fakeServer(func(w http.ResponseWriter, r *http.Request) {
if data.serverErr { if data.serverErr {
http.Error(w, "", http.StatusBadRequest) http.Error(w, "", http.StatusBadRequest)
@ -82,7 +89,7 @@ func TestAcquireGKEProject(t *testing.T) {
}) })
defer ts.Close() defer ts.Close()
boskosURI = ts.URL boskosURI = ts.URL
_, err := AcquireGKEProject(data.host) _, err := client.AcquireGKEProject(data.host)
if data.expErr && (nil == err) { if data.expErr && (nil == err) {
t.Fatalf("testing acquiring GKE project, want: err, got: no err") t.Fatalf("testing acquiring GKE project, want: err, got: no err")
} }
@ -123,6 +130,7 @@ func TestReleaseGKEProject(t *testing.T) {
common.GetOSEnv = oldGetOSEnv common.GetOSEnv = oldGetOSEnv
}() }()
for _, data := range datas { for _, data := range datas {
setup()
ts := fakeServer(func(w http.ResponseWriter, r *http.Request) { ts := fakeServer(func(w http.ResponseWriter, r *http.Request) {
if data.serverErr { if data.serverErr {
http.Error(w, "", http.StatusBadRequest) http.Error(w, "", http.StatusBadRequest)
@ -134,7 +142,7 @@ func TestReleaseGKEProject(t *testing.T) {
}) })
defer ts.Close() defer ts.Close()
boskosURI = ts.URL boskosURI = ts.URL
err := ReleaseGKEProject(data.host, data.resName) err := client.ReleaseGKEProject(data.host, data.resName)
if data.expErr && (nil == err) { if data.expErr && (nil == err) {
t.Fatalf("testing acquiring GKE project, want: err, got: no err") t.Fatalf("testing acquiring GKE project, want: err, got: no err")
} }

View File

@ -0,0 +1,69 @@
/*
Copyright 2019 The Knative 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 fake
import (
"fmt"
boskoscommon "k8s.io/test-infra/boskos/common"
"knative.dev/pkg/testutils/clustermanager/boskos"
)
// FakeBoskosClient implements boskos.Operation
type FakeBoskosClient struct {
resources []*boskoscommon.Resource
}
// AcquireGKEProject fakes to be no op
func (c *FakeBoskosClient) AcquireGKEProject(host *string) (*boskoscommon.Resource, error) {
for _, res := range c.resources {
if res.State == boskoscommon.Free {
res.State = boskoscommon.Busy
return res, nil
}
}
return nil, fmt.Errorf("no GKE project available")
}
// ReleaseGKEProject fakes to be no op
func (c *FakeBoskosClient) ReleaseGKEProject(host *string, name string) error {
if nil == host {
return fmt.Errorf("host has to be set")
}
for _, res := range c.resources {
if res.Name == name {
if res.Owner == *host {
res.Owner = ""
res.State = boskoscommon.Free
return nil
} else {
return fmt.Errorf("Got owner: '%s', expect owner: '%s'", res.Owner, *host)
}
}
}
return fmt.Errorf("resource doesn't exist yet: '%s'", name)
}
// NewGKEProject adds Boskos resources for testing purpose
func (c *FakeBoskosClient) NewGKEProject(name string) {
c.resources = append(c.resources, &boskoscommon.Resource{
Type: boskos.GKEProjectResource,
Name: name,
State: boskoscommon.Free,
})
}

View File

@ -71,6 +71,7 @@ type GKECluster struct {
NeedCleanup bool NeedCleanup bool
Cluster *container.Cluster Cluster *container.Cluster
operations GKESDKOperations operations GKESDKOperations
boskosOps boskos.Operation
} }
// GKESDKOperations wraps GKE SDK related functions // GKESDKOperations wraps GKE SDK related functions
@ -152,12 +153,15 @@ func (gs *GKEClient) Setup(numNodes *int64, nodeType *string, region *string, zo
} }
gc.operations = &GKESDKClient{containerService} gc.operations = &GKESDKClient{containerService}
gc.boskosOps = &boskos.Client{}
return gc return gc
} }
// Initialize sets up GKE SDK client, checks environment for cluster and // Initialize sets up GKE SDK client, checks environment for cluster and
// projects to decide whether use existing cluster/project or creating new ones. // projects to decide whether use existing cluster/project or creating new ones.
func (gc *GKECluster) Initialize() error { func (gc *GKECluster) Initialize() error {
// Try obtain project name via `kubectl`, `gcloud`
if nil == gc.Project { if nil == gc.Project {
if err := gc.checkEnvironment(); nil != err { if err := gc.checkEnvironment(); nil != err {
return fmt.Errorf("failed checking existing cluster: '%v'", err) return fmt.Errorf("failed checking existing cluster: '%v'", err)
@ -165,14 +169,13 @@ func (gc *GKECluster) Initialize() error {
return nil return nil
} }
} }
if nil == gc.Cluster { // Get project name from boskos if running in Prow
if common.IsProw() { if nil == gc.Project && common.IsProw() {
project, err := boskos.AcquireGKEProject(nil) project, err := gc.boskosOps.AcquireGKEProject(nil)
if nil != err { if nil != err {
return fmt.Errorf("failed acquire boskos project: '%v'", err) return fmt.Errorf("failed acquire boskos project: '%v'", err)
}
gc.Project = &project.Name
} }
gc.Project = &project.Name
} }
if nil == gc.Project || "" == *gc.Project { if nil == gc.Project || "" == *gc.Project {
return errors.New("gcp project must be set") return errors.New("gcp project must be set")

View File

@ -27,6 +27,7 @@ import (
"google.golang.org/api/container/v1" "google.golang.org/api/container/v1"
boskosFake "knative.dev/pkg/testutils/clustermanager/boskos/fake"
"knative.dev/pkg/testutils/common" "knative.dev/pkg/testutils/common"
) )
@ -38,6 +39,7 @@ var (
func setupFakeGKECluster() GKECluster { func setupFakeGKECluster() GKECluster {
return GKECluster{ return GKECluster{
operations: newFakeGKESDKClient(), operations: newFakeGKESDKClient(),
boskosOps: &boskosFake.FakeBoskosClient{},
} }
} }
@ -257,6 +259,7 @@ func TestSetup(t *testing.T) {
gotCo := co.(*GKECluster) gotCo := co.(*GKECluster)
// mock for easier comparison // mock for easier comparison
gotCo.operations = nil gotCo.operations = nil
gotCo.boskosOps = nil
if !reflect.DeepEqual(co, data.expClusterOperations) { if !reflect.DeepEqual(co, data.expClusterOperations) {
t.Fatalf("%s\nwant GKECluster:\n'%v'\ngot GKECluster:\n'%v'", errPrefix, data.expClusterOperations, co) t.Fatalf("%s\nwant GKECluster:\n'%v'\ngot GKECluster:\n'%v'", errPrefix, data.expClusterOperations, co)
} }
@ -265,30 +268,45 @@ func TestSetup(t *testing.T) {
func TestInitialize(t *testing.T) { func TestInitialize(t *testing.T) {
customProj := "customproj" customProj := "customproj"
fakeBoskosProj := "fake-boskos-proj-0"
datas := []struct { datas := []struct {
project *string project *string
clusterExist bool clusterExist bool
gcloudSet bool gcloudSet bool
isProw bool
boskosProjs []string
expProj *string expProj *string
expCluster *container.Cluster expCluster *container.Cluster
expErr error expErr error
}{ }{
{ {
// User defines project // User defines project
&fakeProj, false, false, &fakeProj, nil, nil, &fakeProj, false, false, false, []string{}, &fakeProj, nil, nil,
}, {
// User defines project, and running in Prow
&fakeProj, false, false, true, []string{}, &fakeProj, nil, nil,
}, { }, {
// kubeconfig set // kubeconfig set
nil, true, false, &fakeProj, &container.Cluster{ nil, true, false, false, []string{}, &fakeProj, &container.Cluster{
Name: "d", Name: "d",
Location: "c", Location: "c",
Status: "RUNNING", Status: "RUNNING",
}, nil, }, nil,
}, {
// kubeconfig not set and gcloud not set
nil, false, true, &customProj, nil, nil,
}, { }, {
// kubeconfig not set and gcloud set // kubeconfig not set and gcloud set
nil, false, false, nil, nil, fmt.Errorf("gcp project must be set"), nil, false, true, false, []string{}, &customProj, nil, nil,
}, {
// kubeconfig not set and gcloud set, running in Prow and boskos not available
nil, false, false, true, []string{}, nil, nil, fmt.Errorf("failed acquire boskos project: 'no GKE project available'"),
}, {
// kubeconfig not set and gcloud set, running in Prow and boskos available
nil, false, false, true, []string{fakeBoskosProj}, &fakeBoskosProj, nil, nil,
}, {
// kubeconfig not set and gcloud set, not in Prow and boskos not available
nil, false, false, false, []string{}, nil, nil, fmt.Errorf("gcp project must be set"),
}, {
// kubeconfig not set and gcloud set, not in Prow and boskos available
nil, false, false, false, []string{fakeBoskosProj}, nil, nil, fmt.Errorf("gcp project must be set"),
}, },
} }
@ -300,16 +318,6 @@ func TestInitialize(t *testing.T) {
common.StandardExec = oldExecFunc common.StandardExec = oldExecFunc
}() }()
// Mock to make IsProw() always return false, otherwise it will actually
// acquire a boskos project
common.GetOSEnv = func(s string) string {
switch s {
case "PROW_JOB_ID":
return ""
}
return oldEnvFunc(s)
}
for _, data := range datas { for _, data := range datas {
fgc := setupFakeGKECluster() fgc := setupFakeGKECluster()
if nil != data.project { if nil != data.project {
@ -324,6 +332,10 @@ func TestInitialize(t *testing.T) {
ProjectId: parts[1], ProjectId: parts[1],
}) })
} }
// Set up fake boskos
for _, bos := range data.boskosProjs {
fgc.boskosOps.(*boskosFake.FakeBoskosClient).NewGKEProject(bos)
}
// mock for testing // mock for testing
common.StandardExec = func(name string, args ...string) ([]byte, error) { common.StandardExec = func(name string, args ...string) ([]byte, error) {
var out []byte var out []byte
@ -348,12 +360,25 @@ func TestInitialize(t *testing.T) {
} }
return out, err return out, err
} }
// Mock IsProw()
common.GetOSEnv = func(s string) string {
var res string
switch s {
case "PROW_JOB_ID":
if data.isProw {
res = "fake_job_id"
}
default:
res = oldEnvFunc(s)
}
return res
}
err := fgc.Initialize() err := fgc.Initialize()
if !reflect.DeepEqual(err, data.expErr) || !reflect.DeepEqual(fgc.Project, data.expProj) || !reflect.DeepEqual(fgc.Cluster, data.expCluster) { if !reflect.DeepEqual(err, data.expErr) || !reflect.DeepEqual(fgc.Project, data.expProj) || !reflect.DeepEqual(fgc.Cluster, data.expCluster) {
t.Errorf("test initialize with:\n\tpreset project: '%v'\n\tkubeconfig set: '%v'\n\tgcloud set: '%v'\n"+ t.Errorf("test initialize with:\n\tuser defined project: '%v'\n\tkubeconfig set: '%v'\n\tgcloud set: '%v'\n\trunning in prow: '%v'\n\tboskos set: '%v'\n"+
"want:\n\tproject - '%v'\n\tcluster - '%v'\n\terr - '%v'\ngot:\n\tproject - '%v'\n\tcluster - '%v'\n\terr - '%v'", "want:\n\tproject - '%v'\n\tcluster - '%v'\n\terr - '%v'\ngot:\n\tproject - '%v'\n\tcluster - '%v'\n\terr - '%v'",
data.project, data.clusterExist, data.gcloudSet, data.expProj, data.expCluster, data.expErr, fgc.Project, fgc.Cluster, err) data.project, data.clusterExist, data.gcloudSet, data.isProw, data.boskosProjs, data.expProj, data.expCluster, data.expErr, fgc.Project, fgc.Cluster, err)
} }
} }
} }