Merge remote-tracking branch 'origin/master' into containers

This commit is contained in:
ichekrygin 2018-10-04 20:43:30 -07:00
commit dcd52deb2c
17 changed files with 674 additions and 251 deletions

View File

@ -54,4 +54,3 @@ include build/makelib/image.mk
# Generate manifests e.g. CRD, RBAC etc.
manifests:
go run vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go crd

View File

@ -8,5 +8,42 @@ We hope that the items listed below will inspire further engagement from the com
Any dates listed below and the specific issues that will ship in a given milestone are subject to change but should give a general idea of what we are planning.
We use the [milestone](https://github.com/upbound/conductor/milestones) feature in Github so look there for the most up-to-date and issue plan.
## Conductor 0.1
## v0.1
* MySQL support for AWS, GCP, and Azure
* Provider CRDs, credentials management, API/SDK consumption
* Provider specific MySQL CRDs (Amazon RDS, Google Cloud SQL, Microsoft Azure Database for MySQL)
* All 3 big cloud providers will be supported for all resources going forward
* PostgreSQL support for AWS, GCP, and Azure
* same work items as MySQL support
* Controller depth and reliability
* Full CRUD support for all resources (robust lifecycle management)
* CRD status Conditions for status of resources
* Event recording
* Normalized logging using single logging solution (with configurable levels)
* Retry/recovery from failure, idempotence, dealing with partial state
* CI builds/tests/releases
* New isolated jenkins instance (similar to Rook's jenkins)
* Developer unit testing with high code coverage
* Integration testing pipeline
* Artifact publishing (container images, conductor helm chart, etc.)
* Documentation
* User guides, quick-starts, walkthroughs
* Godocs developer docs for source code/packages/libraries
* Open source project management
* [CII best practices checklist](https://bestpractices.coreinfrastructure.org/en/projects/1599#)
* Governance
* Contributor License Agreement (CLA) or Developer Certificate of Origin (DCO)
## v0.2
* Support for other SockShop resources
* MongoDB
* Redis
* RabbitMQ
* Support for Clusters, deploy and manage clusters lifecycle via CRDs
* Federation, deploy resources to target clusters (possibly a new project)
* Performance and Efficiency
* 2-way reconciliation with external resources
* Events/notifications from cloud provider on changes to external resources to trigger reconciliation
* Parallel processing of CRD instances

View File

@ -17,6 +17,7 @@ limitations under the License.
package v1alpha1
import (
"github.com/upbound/conductor/pkg/apis/core/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -25,24 +26,6 @@ func init() {
SchemeBuilder.Register(&RDSInstance{}, &RDSInstanceList{})
}
// RDSInstanceConditionType type for possible conditions the provider could be in.
type RDSInstanceConditionType string
const (
// Pending means that the instance create request has been received and waiting to be fulfilled
Pending RDSInstanceConditionType = "Pending"
// Creating means that the DB instance create request has been processed and DB Instance is being created
Creating RDSInstanceConditionType = "Creating"
// Deleting means that the instance is being deleted.
Deleting RDSInstanceConditionType = "Deleting"
// Failed means that the instance creation has failed.
Failed RDSInstanceConditionType = "Failed"
// Running means that the instance creation has been successful.
Running RDSInstanceConditionType = "Running"
)
type RDSDBInstanceStatus string
// RDSInstanceSpec defines the desired state of RDSInstance
type RDSInstanceSpec struct {
MasterUsername string `json:"masterUsername"`
@ -57,57 +40,11 @@ type RDSInstanceSpec struct {
// RDSInstanceStatus defines the observed state of RDSInstance
type RDSInstanceStatus struct {
v1alpha1.ConditionedStatus
State string `json:"state,omitempty"`
Message string `json:"message,omitempty"`
ProviderID string `json:"providerID,omitempty"` // the external ID to identify this resource in the cloud provider
InstanceName string `json:"instanceName,omitempty"` // the generated DB Instance name
// Conditions indicate state for particular aspects of a CustomResourceDefinition
Conditions []RDSInstanceCondition
}
// GetCondition returns a provider condition with the provided type if it exists.
func (s *RDSInstanceStatus) GetCondition(conditionType RDSInstanceConditionType) *RDSInstanceCondition {
for _, c := range s.Conditions {
if c.Type == conditionType {
return &c
}
}
return nil
}
// SetCondition adds/replaces the given condition in the credentials controller status.
func (s *RDSInstanceStatus) SetCondition(condition RDSInstanceCondition) {
current := s.GetCondition(condition.Type)
if current != nil && current.Status == condition.Status && current.Reason == condition.Reason {
return
}
newConditions := FilterOutCondition(s.Conditions, condition.Type)
s.Conditions = append(newConditions, condition)
}
// UnsetCondition set condition status to false with the given type - if found.
func (s *RDSInstanceStatus) UnsetCondition(conditionType RDSInstanceConditionType) {
current := s.GetCondition(conditionType)
if current != nil && current.Status == corev1.ConditionTrue {
current.Status = corev1.ConditionFalse
s.SetCondition(*current)
}
}
// UnsetAllConditions set conditions status to false on all conditions
func (s *RDSInstanceStatus) UnsetAllConditions() {
var newConditions []RDSInstanceCondition
for _, c := range s.Conditions {
c.Status = corev1.ConditionFalse
newConditions = append(newConditions, c)
}
s.Conditions = newConditions
}
// RemoveCondition removes the condition with the provided type from the credentials controller status.
func (s *RDSInstanceStatus) RemoveCondition(condType RDSInstanceConditionType) {
s.Conditions = FilterOutCondition(s.Conditions, condType)
}
// +genclient
@ -132,41 +69,3 @@ type RDSInstanceList struct {
metav1.ListMeta `json:"metadata,omitempty"`
Items []RDSInstance `json:"items"`
}
// RDSInstanceCondition contains details for the current condition of this pod.
type RDSInstanceCondition struct {
Type RDSInstanceConditionType
Status corev1.ConditionStatus
LastTransitionTime metav1.Time
Reason string
Message string
}
// ProviderStatus defines the observed state of Provider
type ProviderStatus struct {
// Conditions indicate state for particular aspects of a CustomResourceDefinition
Conditions []RDSInstanceCondition
}
// NewCondition creates a new RDS instance condition.
func NewCondition(condType RDSInstanceConditionType, reason, msg string) *RDSInstanceCondition {
return &RDSInstanceCondition{
Type: condType,
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: reason,
Message: msg,
}
}
// FilterOutCondition returns a new slice of credentials controller conditions without conditions with the provided type.
func FilterOutCondition(conditions []RDSInstanceCondition, condType RDSInstanceConditionType) []RDSInstanceCondition {
var newConditions []RDSInstanceCondition
for _, c := range conditions {
if c.Type == condType {
continue
}
newConditions = append(newConditions, c)
}
return newConditions
}

View File

@ -20,32 +20,32 @@ import (
"testing"
"github.com/onsi/gomega"
"golang.org/x/net/context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
func TestStorageRDSInstance(t *testing.T) {
key := types.NamespacedName{Name: "foo", Namespace: "default"}
created := &RDSInstance{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}}
g := gomega.NewGomegaWithT(t)
key := types.NamespacedName{Name: name, Namespace: namespace}
created := &RDSInstance{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}}
// Test Create
fetched := &RDSInstance{}
g.Expect(c.Create(context.TODO(), created)).NotTo(gomega.HaveOccurred())
g.Expect(c.Create(ctx, created)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(fetched).To(gomega.Equal(created))
// Test Updating the Labels
updated := fetched.DeepCopy()
updated.Labels = map[string]string{"hello": "world"}
g.Expect(c.Update(context.TODO(), updated)).NotTo(gomega.HaveOccurred())
g.Expect(c.Update(ctx, updated)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(fetched).To(gomega.Equal(updated))
// Test Delete
g.Expect(c.Delete(context.TODO(), fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(context.TODO(), key, fetched)).To(gomega.HaveOccurred())
g.Expect(c.Delete(ctx, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).To(gomega.HaveOccurred())
}

View File

@ -18,39 +18,40 @@ package v1alpha1
import (
"log"
"os"
"path/filepath"
"testing"
"github.com/upbound/conductor/pkg/test"
"golang.org/x/net/context"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
var cfg *rest.Config
var c client.Client
const (
namespace = "default"
name = "test-instance"
)
var (
crds = []string{filepath.Join("..", "..", "..", "..", "..", "cluster", "charts", "conductor", "crds", "aws", "database", "v1alpha1")}
ctx = context.TODO()
cfg *rest.Config
c client.Client
)
func TestMain(m *testing.M) {
t := &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "..",
"cluster", "charts", "conductor", "crds", "aws", "database", "v1alpha1")},
}
err := SchemeBuilder.AddToScheme(scheme.Scheme)
if err != nil {
log.Fatal(err)
}
if cfg, err = t.Start(); err != nil {
log.Fatal(err)
}
t := test.NewTestEnv(crds, namespace)
cfg = t.Start()
if c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}); err != nil {
log.Fatal(err)
}
code := m.Run()
t.Stop()
os.Exit(code)
t.StopAndExit(m.Run())
}

View File

@ -23,29 +23,6 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderStatus) DeepCopyInto(out *ProviderStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]RDSInstanceCondition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderStatus.
func (in *ProviderStatus) DeepCopy() *ProviderStatus {
if in == nil {
return nil
}
out := new(ProviderStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RDSInstance) DeepCopyInto(out *RDSInstance) {
*out = *in
@ -74,23 +51,6 @@ func (in *RDSInstance) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RDSInstanceCondition) DeepCopyInto(out *RDSInstanceCondition) {
*out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDSInstanceCondition.
func (in *RDSInstanceCondition) DeepCopy() *RDSInstanceCondition {
if in == nil {
return nil
}
out := new(RDSInstanceCondition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RDSInstanceList) DeepCopyInto(out *RDSInstanceList) {
*out = *in
@ -150,13 +110,7 @@ func (in *RDSInstanceSpec) DeepCopy() *RDSInstanceSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RDSInstanceStatus) DeepCopyInto(out *RDSInstanceStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]RDSInstanceCondition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
return
}

View File

@ -20,18 +20,17 @@ import (
"testing"
"github.com/onsi/gomega"
"golang.org/x/net/context"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
func TestStorageProvider(t *testing.T) {
key := types.NamespacedName{Name: "foo", Namespace: "default"}
key := types.NamespacedName{Name: name, Namespace: namespace}
created := &Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "default",
Name: name,
Namespace: namespace,
},
Spec: ProviderSpec{
Secret: v1.SecretKeySelector{
@ -44,21 +43,21 @@ func TestStorageProvider(t *testing.T) {
g := gomega.NewGomegaWithT(t)
// Test Create
g.Expect(c.Create(context.TODO(), created)).NotTo(gomega.HaveOccurred())
g.Expect(c.Create(ctx, created)).NotTo(gomega.HaveOccurred())
fetched := &Provider{}
g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(fetched).To(gomega.Equal(created))
// Test Updating the Labels
updated := fetched.DeepCopy()
updated.Labels = map[string]string{"hello": "world"}
g.Expect(c.Update(context.TODO(), updated)).NotTo(gomega.HaveOccurred())
g.Expect(c.Update(ctx, updated)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(fetched).To(gomega.Equal(updated))
// Test Delete
g.Expect(c.Delete(context.TODO(), fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(context.TODO(), key, fetched)).To(gomega.HaveOccurred())
g.Expect(c.Delete(ctx, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).To(gomega.HaveOccurred())
}

View File

@ -18,39 +18,40 @@ package v1alpha1
import (
"log"
"os"
"path/filepath"
"testing"
"github.com/upbound/conductor/pkg/test"
"golang.org/x/net/context"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
var cfg *rest.Config
var c client.Client
const (
namespace = "default"
name = "test-provider"
)
var (
crds = []string{filepath.Join("..", "..", "..", "..", "cluster", "charts", "conductor", "crds", "aws", "v1alpha1")}
ctx = context.TODO()
cfg *rest.Config
c client.Client
)
func TestMain(m *testing.M) {
t := &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..",
"cluster", "charts", "conductor", "crds", "aws", "v1alpha1")},
}
err := SchemeBuilder.AddToScheme(scheme.Scheme)
if err != nil {
log.Fatal(err)
}
if cfg, err = t.Start(); err != nil {
log.Fatal(err)
}
t := test.NewTestEnv(crds, namespace)
cfg = t.Start()
if c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}); err != nil {
log.Fatal(err)
}
code := m.Run()
t.Stop()
os.Exit(code)
t.StopAndExit(m.Run())
}

View File

@ -0,0 +1,120 @@
/*
Copyright 2018 The Conductor 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 v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ConditionType type for possible conditions the resource could be in.
type ConditionType string
const (
// Pending means that the resource create request has been received and waiting to be fulfilled
Pending ConditionType = "Pending"
// Creating means that the DB resource create request has been processed and managed resource is being created
Creating ConditionType = "Creating"
// Deleting means that the resource is being deleted.
Deleting ConditionType = "Deleting"
// Failed means that the resource creation has failed.
Failed ConditionType = "Failed"
// Running means that the resource creation has been successful.
Running ConditionType = "Running"
)
// Condition contains details for the current condition of this pod.
type Condition struct {
Type ConditionType
Status corev1.ConditionStatus
LastTransitionTime metav1.Time
Reason string
Message string
}
// ConditionedStatus defines the observed state of RDSresource
type ConditionedStatus struct {
// Conditions indicate state for particular aspects of a CustomResourceDefinition
Conditions []Condition
}
// GetCondition returns a provider condition with the provided type if it exists.
func (in *ConditionedStatus) GetCondition(conditionType ConditionType) *Condition {
for _, c := range in.Conditions {
if c.Type == conditionType {
return &c
}
}
return nil
}
// SetCondition adds/replaces the given condition in the credentials controller status.
func (in *ConditionedStatus) SetCondition(condition Condition) {
current := in.GetCondition(condition.Type)
if current != nil && current.Status == condition.Status && current.Reason == condition.Reason {
return
}
newConditions := FilterOutCondition(in.Conditions, condition.Type)
in.Conditions = append(newConditions, condition)
}
// UnsetCondition set condition status to false with the given type - if found.
func (in *ConditionedStatus) UnsetCondition(conditionType ConditionType) {
current := in.GetCondition(conditionType)
if current != nil && current.Status == corev1.ConditionTrue {
current.Status = corev1.ConditionFalse
in.SetCondition(*current)
}
}
// UnsetAllConditions set conditions status to false on all conditions
func (in *ConditionedStatus) UnsetAllConditions() {
var newConditions []Condition
for _, c := range in.Conditions {
c.Status = corev1.ConditionFalse
newConditions = append(newConditions, c)
}
in.Conditions = newConditions
}
// RemoveCondition removes the condition with the provided type from the credentials controller status.
func (in *ConditionedStatus) RemoveCondition(condType ConditionType) {
in.Conditions = FilterOutCondition(in.Conditions, condType)
}
// NewCondition creates a new RDS resource condition.
func NewCondition(condType ConditionType, reason, msg string) *Condition {
return &Condition{
Type: condType,
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: reason,
Message: msg,
}
}
// FilterOutProviderCondition returns a new slice of credentials controller conditions without conditions with the provided type.
func FilterOutCondition(conditions []Condition, condType ConditionType) []Condition {
var newConditions []Condition
for _, c := range conditions {
if c.Type == condType {
continue
}
newConditions = append(newConditions, c)
}
return newConditions
}

View File

@ -45,3 +45,69 @@ type ProviderStatus struct {
// Conditions indicate state for particular aspects of a CustomResourceDefinition
Conditions []ProviderCondition
}
// NewCondition creates a provider condition.
func NewProviderCondition(condType ProviderConditionType, status corev1.ConditionStatus, reason, msg string) *ProviderCondition {
return &ProviderCondition{
Type: condType,
Status: status,
LastTransitionTime: metav1.Now(),
Reason: reason,
Message: msg,
}
}
// GetCondition returns a provider condition with the provided type if it exists.
func (in *ProviderStatus) GetCondition(conditionType ProviderConditionType) *ProviderCondition {
for _, c := range in.Conditions {
if c.Type == conditionType {
return &c
}
}
return nil
}
// SetCondition adds/replaces the given condition in the credentials controller status.
func (in *ProviderStatus) SetCondition(condition ProviderCondition) {
current := in.GetCondition(condition.Type)
if current != nil && current.Status == condition.Status && current.Reason == condition.Reason {
return
}
newConditions := FilterOutProviderCondition(in.Conditions, condition.Type)
in.Conditions = append(newConditions, condition)
}
// SetInvalid condition and unset valid condition
func (in *ProviderStatus) SetInvalid(reason, msg string) {
in.SetCondition(*NewProviderCondition(Invalid, corev1.ConditionTrue, reason, msg))
if valid := in.GetCondition(Valid); valid != nil {
in.SetCondition(*NewProviderCondition(Valid, corev1.ConditionFalse, "", valid.Message))
}
}
// SetValid condition and unset invalid condition
func (in *ProviderStatus) SetValid(msg string) {
in.SetCondition(*NewProviderCondition(Valid, corev1.ConditionTrue, "", msg))
if invalid := in.GetCondition(Invalid); invalid != nil {
in.SetCondition(*NewProviderCondition(Invalid, corev1.ConditionFalse, invalid.Reason, invalid.Message))
}
}
// RemoveCondition removes the condition with the provided type from the credentials controller status.
func (in *ProviderStatus) RemoveCondition(condType ProviderConditionType) {
in.Conditions = FilterOutProviderCondition(in.Conditions, condType)
}
// FilterOutProviderCondition returns a new slice of credentials controller conditions without conditions with the provided type.
func FilterOutProviderCondition(conditions []ProviderCondition, condType ProviderConditionType) []ProviderCondition {
var newConditions []ProviderCondition
for _, c := range conditions {
if c.Type == condType {
continue
}
newConditions = append(newConditions, c)
}
return newConditions
}

View File

@ -0,0 +1,198 @@
/*
Copyright 2018 The Conductor 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 v1alpha1
import (
"testing"
"time"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
)
func TestFilterOutCondition(t *testing.T) {
g := NewGomegaWithT(t)
var empty []ProviderCondition
validOnly := append(empty, *NewProviderCondition(Valid, corev1.ConditionTrue, "", ""))
invalidOnly := append(empty, *NewProviderCondition(Invalid, corev1.ConditionTrue, "", ""))
mixed := append(validOnly, invalidOnly...)
mixedWithDuplicates := append(mixed, mixed...)
// empty - any
g.Expect(FilterOutProviderCondition(empty, Valid)).To(BeNil())
// {valid} - invalid = {valid}
g.Expect(FilterOutProviderCondition(validOnly, Invalid)).To(Equal(validOnly))
// {valid} - valid = nil}
g.Expect(FilterOutProviderCondition(validOnly, Valid)).To(BeNil())
// {valid, invalid} - invalid = {valid}
g.Expect(FilterOutProviderCondition(mixed, Invalid)).To(Equal(validOnly))
// {valid, invalid} - valid = {invalid}
g.Expect(FilterOutProviderCondition(mixed, Valid)).To(Equal(invalidOnly))
// {valid,invalid,valid,invalid} - invalid = {valid,valid}
c := FilterOutProviderCondition(mixedWithDuplicates, Invalid)
g.Expect(c).To(Equal(append(validOnly, validOnly...)))
// {valid,valid} - invalid = {valid, valid} (no change)
c = FilterOutProviderCondition(c, Invalid)
g.Expect(c).To(Equal(append(validOnly, validOnly...)))
// {valid,valid} - valid = {nil}
g.Expect(FilterOutProviderCondition(c, Valid)).To(BeNil())
}
func TestRemoveCondition(t *testing.T) {
g := NewGomegaWithT(t)
status := &ProviderStatus{}
g.Expect(status.Conditions).To(BeNil())
status.RemoveCondition(Valid)
g.Expect(status.Conditions).To(BeNil())
conditions := []ProviderCondition{*NewProviderCondition(Valid, corev1.ConditionTrue, "", "")}
status.SetCondition(conditions[0])
g.Expect(status.Conditions).To(Equal(conditions))
status.RemoveCondition(Invalid)
g.Expect(status.Conditions).To(Equal(conditions))
status.RemoveCondition(Valid)
g.Expect(status.Conditions).To(BeNil())
}
func TestGetConditions(t *testing.T) {
g := NewGomegaWithT(t)
status := &ProviderStatus{}
g.Expect(status.Conditions).To(BeNil())
c := status.GetCondition(Invalid)
g.Expect(c).To(BeNil())
st := time.Now()
status.SetCondition(*NewProviderCondition(Valid, corev1.ConditionTrue, "", ""))
g.Expect(status.Conditions).To(Not(BeNil()))
c = status.GetCondition(Invalid)
g.Expect(c).To(BeNil())
c = status.GetCondition(Valid)
g.Expect(c.Type).To(Equal(Valid))
g.Expect(c.Status).To(Equal(corev1.ConditionTrue))
g.Expect(c.LastTransitionTime.After(st)).To(BeTrue())
}
func TestSetConditions(t *testing.T) {
g := NewGomegaWithT(t)
status := &ProviderStatus{}
g.Expect(status.Conditions).To(BeNil())
valid := *NewProviderCondition(Valid, corev1.ConditionTrue, "", "")
status.SetCondition(valid)
g.Expect(status.Conditions).To(Equal([]ProviderCondition{valid}))
invalid := *NewProviderCondition(Invalid, corev1.ConditionFalse, "Invalid reason", "")
status.SetCondition(invalid)
g.Expect(status.Conditions).To(Equal([]ProviderCondition{valid, invalid}))
// new valid - diff message only - no change
newValid := *NewProviderCondition(Valid, corev1.ConditionTrue, "", "bar")
status.SetCondition(newValid)
g.Expect(status.Conditions).To(Equal([]ProviderCondition{valid, invalid}))
// new valid - diff reason and message - change
newValid.Reason = "foo"
valid = newValid
status.SetCondition(newValid)
g.Expect(status.Conditions).To(Equal([]ProviderCondition{invalid, valid}))
// new valid - diff Status - change
newValid.Status = corev1.ConditionUnknown
valid = newValid
status.SetCondition(newValid)
g.Expect(status.Conditions).To(Equal([]ProviderCondition{invalid, valid}))
}
func TestSetInvalid(t *testing.T) {
g := NewGomegaWithT(t)
status := &ProviderStatus{}
g.Expect(status.Conditions).To(BeNil())
ts := time.Now()
status.SetInvalid("fail", "bye")
i := status.GetCondition(Invalid)
g.Expect(i).To(Not(BeNil()))
g.Expect(i.Status).To(Equal(corev1.ConditionTrue))
g.Expect(i.Reason).To(Equal("fail"))
g.Expect(i.Message).To(Equal("bye"))
g.Expect(i.LastTransitionTime.After(ts)).To(BeTrue())
v := status.GetCondition(Valid)
g.Expect(v).To(BeNil())
status.RemoveCondition(Invalid)
g.Expect(status.Conditions).To(BeNil())
valid := *NewProviderCondition(Valid, corev1.ConditionTrue, "", "")
status.SetCondition(valid)
ts = time.Now()
status.SetInvalid("fail", "bye")
i = status.GetCondition(Invalid)
g.Expect(i).To(Not(BeNil()))
g.Expect(i.Status).To(Equal(corev1.ConditionTrue))
g.Expect(i.Reason).To(Equal("fail"))
g.Expect(i.Message).To(Equal("bye"))
g.Expect(i.LastTransitionTime.After(ts)).To(BeTrue())
v = status.GetCondition(Valid)
g.Expect(v).To(Not(BeNil()))
g.Expect(v.Status).To(Equal(corev1.ConditionFalse))
g.Expect(v.LastTransitionTime.After(ts)).To(BeTrue())
}
func TestSetValid(t *testing.T) {
g := NewGomegaWithT(t)
status := &ProviderStatus{}
g.Expect(status.Conditions).To(BeNil())
ts := time.Now()
status.SetValid("hello")
g.Expect(len(status.Conditions)).To(Equal(1))
v := status.GetCondition(Valid)
g.Expect(v).To(Not(BeNil()))
g.Expect(v.Status).To(Equal(corev1.ConditionTrue))
g.Expect(v.Reason).To(Equal(""))
g.Expect(v.Message).To(Equal("hello"))
g.Expect(v.LastTransitionTime.After(ts)).To(BeTrue())
i := status.GetCondition(Invalid)
g.Expect(i).To(BeNil())
status.RemoveCondition(Valid)
g.Expect(status.Conditions).To(BeNil())
invalid := *NewProviderCondition(Invalid, corev1.ConditionTrue, "fail", "")
status.SetCondition(invalid)
ts = time.Now()
status.SetValid("hello")
v = status.GetCondition(Valid)
g.Expect(v).To(Not(BeNil()))
g.Expect(v.Status).To(Equal(corev1.ConditionTrue))
g.Expect(v.Reason).To(Equal(""))
g.Expect(v.Message).To(Equal("hello"))
g.Expect(v.LastTransitionTime.After(ts)).To(BeTrue())
i = status.GetCondition(Invalid)
g.Expect(i.Status).To(Equal(corev1.ConditionFalse))
}

View File

@ -19,6 +19,46 @@ limitations under the License.
package v1alpha1
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Condition) DeepCopyInto(out *Condition) {
*out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.
func (in *Condition) DeepCopy() *Condition {
if in == nil {
return nil
}
out := new(Condition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ConditionedStatus) DeepCopyInto(out *ConditionedStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConditionedStatus.
func (in *ConditionedStatus) DeepCopy() *ConditionedStatus {
if in == nil {
return nil
}
out := new(ConditionedStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderCondition) DeepCopyInto(out *ProviderCondition) {
*out = *in

View File

@ -20,32 +20,31 @@ import (
"testing"
"github.com/onsi/gomega"
"golang.org/x/net/context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
func TestStorageCloudsqlInstance(t *testing.T) {
key := types.NamespacedName{Name: "foo", Namespace: "default"}
created := &CloudsqlInstance{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}}
key := types.NamespacedName{Name: name, Namespace: namespace}
created := &CloudsqlInstance{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}}
g := gomega.NewGomegaWithT(t)
// Test Create
fetched := &CloudsqlInstance{}
g.Expect(c.Create(context.TODO(), created)).NotTo(gomega.HaveOccurred())
g.Expect(c.Create(ctx, created)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(fetched).To(gomega.Equal(created))
// Test Updating the Labels
updated := fetched.DeepCopy()
updated.Labels = map[string]string{"hello": "world"}
g.Expect(c.Update(context.TODO(), updated)).NotTo(gomega.HaveOccurred())
g.Expect(c.Update(ctx, updated)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(fetched).To(gomega.Equal(updated))
// Test Delete
g.Expect(c.Delete(context.TODO(), fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(context.TODO(), key, fetched)).To(gomega.HaveOccurred())
g.Expect(c.Delete(ctx, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).To(gomega.HaveOccurred())
}

View File

@ -17,40 +17,41 @@ limitations under the License.
package v1alpha1
import (
"context"
"log"
"os"
"path/filepath"
"testing"
"github.com/upbound/conductor/pkg/test"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
var cfg *rest.Config
var c client.Client
const (
namespace = "default"
name = "test-instance"
)
var (
cfg *rest.Config
c client.Client
crds = []string{filepath.Join("..", "..", "..", "..", "..", "cluster", "charts", "conductor", "crds", "gcp", "database", "v1alpha1")}
ctx = context.TODO()
)
func TestMain(m *testing.M) {
t := &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "..",
"cluster", "charts", "conductor", "crds", "gcp", "database", "v1alpha1")},
}
err := SchemeBuilder.AddToScheme(scheme.Scheme)
if err != nil {
log.Fatal(err)
}
if cfg, err = t.Start(); err != nil {
log.Fatal(err)
}
t := test.NewTestEnv(crds, namespace)
cfg = t.Start()
if c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}); err != nil {
log.Fatal(err)
}
code := m.Run()
t.Stop()
os.Exit(code)
t.StopAndExit(m.Run())
}

View File

@ -20,23 +20,22 @@ import (
"testing"
"github.com/onsi/gomega"
"golang.org/x/net/context"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
func TestStorageProvider(t *testing.T) {
key := types.NamespacedName{Name: "foo", Namespace: "default"}
key := types.NamespacedName{Name: name, Namespace: namespace}
created := &Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "default",
Name: name,
Namespace: namespace,
},
Spec: ProviderSpec{
SecretKey: v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{Name: "u-235"},
Key: "credentials.json",
Key: secretDataKey,
},
ProjectID: "manhattan",
RequiredPermissions: []string{"crate", "update", "delete"},
@ -45,21 +44,21 @@ func TestStorageProvider(t *testing.T) {
g := gomega.NewGomegaWithT(t)
// Test Create
g.Expect(c.Create(context.TODO(), created)).NotTo(gomega.HaveOccurred())
g.Expect(c.Create(ctx, created)).NotTo(gomega.HaveOccurred())
fetched := &Provider{}
g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(fetched).To(gomega.Equal(created))
// Test Updating the Labels
updated := fetched.DeepCopy()
updated.Labels = map[string]string{"hello": "world"}
g.Expect(c.Update(context.TODO(), updated)).NotTo(gomega.HaveOccurred())
g.Expect(c.Update(ctx, updated)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(fetched).To(gomega.Equal(updated))
// Test Delete
g.Expect(c.Delete(context.TODO(), fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(context.TODO(), key, fetched)).To(gomega.HaveOccurred())
g.Expect(c.Delete(ctx, fetched)).NotTo(gomega.HaveOccurred())
g.Expect(c.Get(ctx, key, fetched)).To(gomega.HaveOccurred())
}

View File

@ -17,40 +17,42 @@ limitations under the License.
package v1alpha1
import (
"context"
"log"
"os"
"path/filepath"
"testing"
"github.com/upbound/conductor/pkg/test"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
var cfg *rest.Config
var c client.Client
const (
namespace = "default"
name = "test-provider"
secretDataKey = "credentials.json"
)
var (
cfg *rest.Config
c client.Client
crds = []string{filepath.Join("..", "..", "..", "..", "cluster", "charts", "conductor", "crds", "gcp", "v1alpha1")}
ctx = context.TODO()
)
func TestMain(m *testing.M) {
t := &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..",
"cluster", "charts", "conductor", "crds", "gcp", "v1alpha1")},
}
err := SchemeBuilder.AddToScheme(scheme.Scheme)
if err != nil {
log.Fatal(err)
}
if cfg, err = t.Start(); err != nil {
log.Fatal(err)
}
t := test.NewTestEnv(crds, namespace)
cfg = t.Start()
if c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}); err != nil {
log.Fatal(err)
}
code := m.Run()
t.Stop()
os.Exit(code)
t.StopAndExit(m.Run())
}

108
pkg/test/envtest.go Normal file
View File

@ -0,0 +1,108 @@
package test
import (
"log"
"os"
"strconv"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
const (
DEFAULT_NAMESPACE = "default"
TEST_ASSET_USE_EXISTING_CLUSTER = "USE_EXISTING_CLUSTER"
)
// TestEnv - wrapper for controller-runtime envtest with additional functionality
type TestEnv struct {
envtest.Environment
namespace string
cfg *rest.Config
}
// NewTestEnv - create new test environment instance
func NewTestEnv(crds []string, namespace string) *TestEnv {
t := envtest.Environment{
UseExistingCluster: UseExistingCluster(),
}
if !t.UseExistingCluster {
if err := CheckCRDFiles(crds); err != nil {
log.Panic(err)
}
t.CRDDirectoryPaths = crds
}
if len(namespace) == 0 {
namespace = "default"
}
return &TestEnv{
Environment: t,
namespace: namespace,
}
}
// Start - starts and bootstraps test environment ang returns Kubernetes config instance
func (te *TestEnv) Start() *rest.Config {
cfg, err := te.Environment.Start()
if err != nil {
log.Fatal(err)
}
te.cfg = cfg
// Crate testing namespace for this package if it is not "default"
if te.namespace != "default" {
k := kubernetes.NewForConfigOrDie(cfg)
_, err := k.CoreV1().Namespaces().Create(&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: te.namespace,
},
})
if err != nil && !errors.IsAlreadyExists(err) {
log.Fatal(err)
}
}
return cfg
}
// Stop - stops test environment performing additional cleanup (if needed)
func (te *TestEnv) Stop() {
defer te.Environment.Stop()
if te.namespace != DEFAULT_NAMESPACE {
k := kubernetes.NewForConfigOrDie(te.cfg)
dp := metav1.DeletePropagationForeground
err := k.CoreV1().Namespaces().Delete(te.namespace, &metav1.DeleteOptions{PropagationPolicy: &dp})
if err != nil {
log.Panic(err)
}
}
}
// StopAndExit - stops and exists, typically used as a last call in TestMain
func (te *TestEnv) StopAndExit(code int) {
te.Stop()
os.Exit(code)
}
// UseExistingCluster - checks if USE_EXISTING_CLUSTER environment variable is set
func UseExistingCluster() bool {
env, err := strconv.ParseBool(os.Getenv(TEST_ASSET_USE_EXISTING_CLUSTER))
if err != nil {
return false
}
return env
}
// CheckCRDFiles - validates that all crds files are found.
func CheckCRDFiles(crds []string) error {
for _, path := range crds {
if _, err := os.Stat(path); os.IsNotExist(err) {
return err
}
}
return nil
}