diff --git a/Makefile b/Makefile index a502435bd..e1597a2de 100644 --- a/Makefile +++ b/Makefile @@ -88,6 +88,7 @@ test/e2e/image: .PHONY: test/unit test/unit: @echo Running tests: + go get -u github.com/rakyll/gotest gotest -v -covermode=count -coverprofile=coverage.out ./pkg/controller/... ./pkg/providers/... ./pkg/resources/... ./pkg/apis/integreatly/v1alpha1/types/... .PHONY: test/unit/coverage diff --git a/go.mod b/go.mod index fac4e7e20..2e64aea50 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/prometheus/client_golang v1.1.0 // indirect github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect github.com/prometheus/procfs v0.0.4 // indirect - github.com/rakyll/gotest v0.0.0-20180125184505-86f0749cd8cc + github.com/rakyll/gotest v0.0.0-20191108192113-45d501058f2a github.com/sirupsen/logrus v1.4.2 github.com/spf13/pflag v1.0.3 github.com/stretchr/testify v1.4.0 // indirect @@ -31,6 +31,7 @@ require ( golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 // indirect golang.org/x/net v0.0.0-20191003171128-d98b1b443823 // indirect golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect + golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4 // indirect golang.org/x/tools v0.0.0-20190924052046-3ac2a5bbd98a // indirect google.golang.org/appengine v1.6.2 // indirect google.golang.org/grpc v1.22.0 // indirect diff --git a/go.sum b/go.sum index 6638ee43b..0c9bdf4fb 100644 --- a/go.sum +++ b/go.sum @@ -382,6 +382,8 @@ github.com/prometheus/procfs v0.0.4 h1:w8DjqFMJDjuVwdZBQoOozr4MVWOnwF7RcL/7uxBjY github.com/prometheus/procfs v0.0.4/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/rakyll/gotest v0.0.0-20180125184505-86f0749cd8cc h1:hrzpgS8mnUi65ieVrD3TKJMxHP84bzmybMTQIdK/XhM= github.com/rakyll/gotest v0.0.0-20180125184505-86f0749cd8cc/go.mod h1:iln+RRtJaJ52lKwqrSmNgQYw32Fk16CgChX85eFqBgI= +github.com/rakyll/gotest v0.0.0-20191108192113-45d501058f2a h1:/kX+lZpr87Pb0yJKyxW40ZZO6jl52jFMmoQ0YhfwGLM= +github.com/rakyll/gotest v0.0.0-20191108192113-45d501058f2a/go.mod h1:jpFrc1UTqK0FtfF3doi3pEUBgWHYELkOPPECUlDsM2Q= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/robfig/cron v0.0.0-20170526150127-736158dc09e1/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -538,6 +540,8 @@ golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 h1:rOhMmluY6kLMhdnrivzec6lLg golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4 h1:Hynbrlo6LbYI3H1IqXpkVDOcX/3HiPdhVEuyj5a59RM= +golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/pkg/controller/blobstorage/blobstorage_controller.go b/pkg/controller/blobstorage/blobstorage_controller.go index 2ef2b1bff..1a06dc126 100644 --- a/pkg/controller/blobstorage/blobstorage_controller.go +++ b/pkg/controller/blobstorage/blobstorage_controller.go @@ -3,7 +3,9 @@ package blobstorage import ( "context" "fmt" + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1/types" + "github.com/integr8ly/cloud-resource-operator/pkg/providers/openshift" "github.com/integr8ly/cloud-resource-operator/pkg/resources" "github.com/sirupsen/logrus" @@ -37,7 +39,7 @@ func Add(mgr manager.Manager) error { func newReconciler(mgr manager.Manager) reconcile.Reconciler { client := mgr.GetClient() logger := logrus.WithFields(logrus.Fields{"controller": "controller_blobstorage"}) - providerList := []providers.BlobStorageProvider{aws.NewAWSBlobStorageProvider(client, logger)} + providerList := []providers.BlobStorageProvider{aws.NewAWSBlobStorageProvider(client, logger), openshift.NewBlobStorageProvider(client, logger)} rp := resources.NewResourceProvider(client, mgr.GetScheme(), logger) return &ReconcileBlobStorage{ client: client, diff --git a/pkg/controller/smtpcredentialset/smtpcredentialset_controller.go b/pkg/controller/smtpcredentialset/smtpcredentialset_controller.go index 24392484b..0ee400282 100644 --- a/pkg/controller/smtpcredentialset/smtpcredentialset_controller.go +++ b/pkg/controller/smtpcredentialset/smtpcredentialset_controller.go @@ -2,7 +2,9 @@ package smtpcredentialset import ( "context" + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1/types" + "github.com/integr8ly/cloud-resource-operator/pkg/providers/openshift" "github.com/integr8ly/cloud-resource-operator/pkg/resources" "github.com/integr8ly/cloud-resource-operator/pkg/providers" @@ -35,7 +37,7 @@ func Add(mgr manager.Manager) error { func newReconciler(mgr manager.Manager) reconcile.Reconciler { client := mgr.GetClient() logger := logrus.WithFields(logrus.Fields{"controller": "controller_smtpcredentialset"}) - providerList := []providers.SMTPCredentialsProvider{aws.NewAWSSMTPCredentialProvider(client, logger)} + providerList := []providers.SMTPCredentialsProvider{aws.NewAWSSMTPCredentialProvider(client, logger), openshift.NewSMTPCredentialSetProvider(client, logger)} rp := resources.NewResourceProvider(client, mgr.GetScheme(), logger) return &ReconcileSMTPCredentialSet{ client: mgr.GetClient(), diff --git a/pkg/providers/aws/provider_blobstorage.go b/pkg/providers/aws/provider_blobstorage.go index 3143ffa09..a539f008e 100644 --- a/pkg/providers/aws/provider_blobstorage.go +++ b/pkg/providers/aws/provider_blobstorage.go @@ -38,11 +38,11 @@ const ( blobstorageProviderName = "aws-s3" defaultAwsBucketNameLength = 40 // default create options - dataBucketName = "bucketName" - dataBucketRegion = "bucketRegion" - dataCredentialKeyID = "credentialKeyID" - dataCredentialSecretKey = "credentialSecretKey" - defaultForceBucketDeletion = false + DetailsBlobStorageBucketName = "bucketName" + DetailsBlobStorageBucketRegion = "bucketRegion" + DetailsBlobStorageCredentialKeyID = "credentialKeyID" + DetailsBlobStorageCredentialSecretKey = "credentialSecretKey" + defaultForceBucketDeletion = false ) // BlobStorageDeploymentDetails Provider-specific details about the AWS S3 bucket created @@ -55,10 +55,10 @@ type BlobStorageDeploymentDetails struct { func (d *BlobStorageDeploymentDetails) Data() map[string][]byte { return map[string][]byte{ - dataBucketName: []byte(d.BucketName), - dataBucketRegion: []byte(d.BucketRegion), - dataCredentialKeyID: []byte(d.CredentialKeyID), - dataCredentialSecretKey: []byte(d.CredentialSecretKey), + DetailsBlobStorageBucketName: []byte(d.BucketName), + DetailsBlobStorageBucketRegion: []byte(d.BucketRegion), + DetailsBlobStorageCredentialKeyID: []byte(d.CredentialKeyID), + DetailsBlobStorageCredentialSecretKey: []byte(d.CredentialSecretKey), } } diff --git a/pkg/providers/aws/provider_smtpcredentialset.go b/pkg/providers/aws/provider_smtpcredentialset.go index 3bfda9b37..978bab213 100644 --- a/pkg/providers/aws/provider_smtpcredentialset.go +++ b/pkg/providers/aws/provider_smtpcredentialset.go @@ -29,11 +29,11 @@ import ( const ( smtpCredentialProviderName = "aws-ses" // default create options - detailsSMTPUsernameKey = "username" - detailsSMTPPasswordKey = "password" - detailsSMTPPortKey = "port" - detailsSMTPHostKey = "host" - detailsSMTPTLSKey = "tls" + DetailsSMTPUsernameKey = "username" + DetailsSMTPPasswordKey = "password" + DetailsSMTPPortKey = "port" + DetailsSMTPHostKey = "host" + DetailsSMTPTLSKey = "tls" ) // SMTPCredentialSetDetails Provider-specific details about SMTP credentials derived from an AWS IAM role @@ -47,11 +47,11 @@ type SMTPCredentialSetDetails struct { func (d *SMTPCredentialSetDetails) Data() map[string][]byte { return map[string][]byte{ - detailsSMTPUsernameKey: []byte(d.Username), - detailsSMTPPasswordKey: []byte(d.Password), - detailsSMTPPortKey: []byte(strconv.Itoa(d.Port)), - detailsSMTPHostKey: []byte(d.Host), - detailsSMTPTLSKey: []byte(strconv.FormatBool(d.TLS)), + DetailsSMTPUsernameKey: []byte(d.Username), + DetailsSMTPPasswordKey: []byte(d.Password), + DetailsSMTPPortKey: []byte(strconv.Itoa(d.Port)), + DetailsSMTPHostKey: []byte(d.Host), + DetailsSMTPTLSKey: []byte(strconv.FormatBool(d.TLS)), } } @@ -139,7 +139,7 @@ func (p *SMTPCredentialProvider) CreateSMTPCredentials(ctx context.Context, smtp DeploymentDetails: &SMTPCredentialSetDetails{ Username: sendMailCreds.AccessKeyID, Password: smtpPass, - Port: 465, + Port: 587, Host: sesSMTPHost, TLS: true, }, diff --git a/pkg/providers/aws/provider_smtpcredentialset_test.go b/pkg/providers/aws/provider_smtpcredentialset_test.go index bafc191d3..71f44f7b9 100644 --- a/pkg/providers/aws/provider_smtpcredentialset_test.go +++ b/pkg/providers/aws/provider_smtpcredentialset_test.go @@ -339,11 +339,11 @@ func TestSMTPCredentialProvider_CreateSMTPCredentials(t *testing.T) { return nil }, wantData: map[string][]byte{ - detailsSMTPUsernameKey: []byte("test"), - detailsSMTPPasswordKey: []byte("AsuNxtdhciTpIaQYwF9CtO/nlNX2hCZkD8E+4vZzrjs0"), - detailsSMTPPortKey: []byte("465"), - detailsSMTPHostKey: []byte(sesSMTPEndpointEUWest1), - detailsSMTPTLSKey: []byte("true"), + DetailsSMTPUsernameKey: []byte("test"), + DetailsSMTPPasswordKey: []byte("AsuNxtdhciTpIaQYwF9CtO/nlNX2hCZkD8E+4vZzrjs0"), + DetailsSMTPPortKey: []byte("587"), + DetailsSMTPHostKey: []byte(sesSMTPEndpointEUWest1), + DetailsSMTPTLSKey: []byte("true"), }, }, { diff --git a/pkg/providers/openshift/provider_blobstorage.go b/pkg/providers/openshift/provider_blobstorage.go new file mode 100644 index 000000000..e103bf4bf --- /dev/null +++ b/pkg/providers/openshift/provider_blobstorage.go @@ -0,0 +1,83 @@ +package openshift + +import ( + "context" + "time" + + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1" + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1/types" + "github.com/integr8ly/cloud-resource-operator/pkg/providers" + "github.com/integr8ly/cloud-resource-operator/pkg/providers/aws" + "github.com/integr8ly/cloud-resource-operator/pkg/resources" + "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + varPlaceholder = "REPLACE_ME" +) + +var _ providers.BlobStorageProvider = (*BlobStorageProvider)(nil) + +type BlobStorageProvider struct { + Client client.Client + Logger *logrus.Entry +} + +func NewBlobStorageProvider(c client.Client, l *logrus.Entry) *BlobStorageProvider { + return &BlobStorageProvider{ + Client: c, + Logger: l, + } +} + +func (b BlobStorageProvider) GetName() string { + return "openshift-blobstorage" +} + +func (b BlobStorageProvider) SupportsStrategy(s string) bool { + return providers.OpenShiftDeploymentStrategy == s +} + +func (b BlobStorageProvider) GetReconcileTime(bs *v1alpha1.BlobStorage) time.Duration { + return time.Second * 10 +} + +func (b BlobStorageProvider) CreateStorage(ctx context.Context, bs *v1alpha1.BlobStorage) (*providers.BlobStorageInstance, types.StatusMessage, error) { + // default to an empty s3 set of credentials for now. in the future. this should determine the cloud provider being + // used by checking the infrastructure cr. + dd := &aws.BlobStorageDeploymentDetails{ + BucketName: varPlaceholder, + BucketRegion: varPlaceholder, + CredentialKeyID: varPlaceholder, + CredentialSecretKey: varPlaceholder, + } + + if bs.Spec.SecretRef.Namespace == "" { + bs.Spec.SecretRef.Namespace = bs.Namespace + } + + if bs.Status.Phase != types.PhaseComplete || bs.Status.SecretRef.Name == "" || bs.Status.SecretRef.Namespace == "" { + return &providers.BlobStorageInstance{ + DeploymentDetails: dd, + }, "reconcile complete", nil + } + + sec := &v1.Secret{} + if err := b.Client.Get(ctx, client.ObjectKey{Name: bs.Status.SecretRef.Name, Namespace: bs.Status.SecretRef.Namespace}, sec); err != nil { + return nil, "failed to reconcile", err + } + + dd.BucketName = resources.StringOrDefault(string(sec.Data[aws.DetailsBlobStorageBucketName]), varPlaceholder) + dd.BucketRegion = resources.StringOrDefault(string(sec.Data[aws.DetailsBlobStorageBucketRegion]), varPlaceholder) + dd.CredentialKeyID = resources.StringOrDefault(string(sec.Data[aws.DetailsBlobStorageCredentialKeyID]), varPlaceholder) + dd.CredentialSecretKey = resources.StringOrDefault(string(sec.Data[aws.DetailsBlobStorageCredentialSecretKey]), varPlaceholder) + return &providers.BlobStorageInstance{ + DeploymentDetails: dd, + }, "reconcile complete", nil +} + +func (b BlobStorageProvider) DeleteStorage(ctx context.Context, bs *v1alpha1.BlobStorage) (types.StatusMessage, error) { + return "deletion complete", nil +} diff --git a/pkg/providers/openshift/provider_blobstorage_test.go b/pkg/providers/openshift/provider_blobstorage_test.go new file mode 100644 index 000000000..01b2a22bf --- /dev/null +++ b/pkg/providers/openshift/provider_blobstorage_test.go @@ -0,0 +1,250 @@ +package openshift + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1" + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1/types" + "github.com/integr8ly/cloud-resource-operator/pkg/providers" + "github.com/integr8ly/cloud-resource-operator/pkg/providers/aws" + "github.com/sirupsen/logrus" + v12 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestBlobStorageProvider_CreateStorage(t *testing.T) { + type fields struct { + Client client.Client + Logger *logrus.Entry + } + type args struct { + ctx context.Context + bs *v1alpha1.BlobStorage + } + tests := []struct { + name string + fields fields + args args + want *providers.BlobStorageInstance + wantErr bool + }{ + { + name: "test secret is created", + fields: fields{ + Client: fake.NewFakeClient(), + Logger: &logrus.Entry{}, + }, + args: args{ + ctx: context.TODO(), + bs: &v1alpha1.BlobStorage{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: v1alpha1.BlobStorageSpec{ + SecretRef: &types.SecretRef{ + Name: "test-sec", + Namespace: "", + }, + }, + Status: v1alpha1.BlobStorageStatus{}, + }, + }, + want: &providers.BlobStorageInstance{ + DeploymentDetails: &aws.BlobStorageDeploymentDetails{ + BucketName: varPlaceholder, + BucketRegion: varPlaceholder, + CredentialKeyID: varPlaceholder, + CredentialSecretKey: varPlaceholder, + }, + }, + wantErr: false, + }, + { + name: "test existing secret is not overridden", + fields: fields{ + Client: fake.NewFakeClient(&v12.Secret{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Data: map[string][]byte{ + aws.DetailsBlobStorageBucketName: []byte("test"), + aws.DetailsBlobStorageBucketRegion: []byte("test"), + aws.DetailsBlobStorageCredentialKeyID: []byte("test"), + aws.DetailsBlobStorageCredentialSecretKey: []byte("test"), + }, + }), + Logger: &logrus.Entry{}, + }, + args: args{ + ctx: context.TODO(), + bs: &v1alpha1.BlobStorage{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: v1alpha1.BlobStorageSpec{ + SecretRef: &types.SecretRef{ + Name: "test-sec", + Namespace: "", + }, + }, + Status: v1alpha1.BlobStorageStatus{ + Phase: types.PhaseComplete, + SecretRef: &types.SecretRef{ + Name: "test", + Namespace: "test", + }, + }, + }, + }, + want: &providers.BlobStorageInstance{ + DeploymentDetails: &aws.BlobStorageDeploymentDetails{ + BucketName: "test", + BucketRegion: "test", + CredentialKeyID: "test", + CredentialSecretKey: "test", + }, + }, + wantErr: false, + }, + { + name: "test missing secret values are reset", + fields: fields{ + Client: fake.NewFakeClient(&v12.Secret{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Data: map[string][]byte{ + aws.DetailsBlobStorageCredentialKeyID: []byte("test"), + }, + }), + Logger: &logrus.Entry{}, + }, + args: args{ + ctx: context.TODO(), + bs: &v1alpha1.BlobStorage{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: v1alpha1.BlobStorageSpec{ + SecretRef: &types.SecretRef{ + Name: "test-sec", + Namespace: "", + }, + }, + Status: v1alpha1.BlobStorageStatus{ + Phase: types.PhaseComplete, + SecretRef: &types.SecretRef{ + Name: "test", + Namespace: "test", + }, + }, + }, + }, + want: &providers.BlobStorageInstance{ + DeploymentDetails: &aws.BlobStorageDeploymentDetails{ + BucketName: varPlaceholder, + BucketRegion: varPlaceholder, + CredentialKeyID: "test", + CredentialSecretKey: varPlaceholder, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := BlobStorageProvider{ + Client: tt.fields.Client, + Logger: tt.fields.Logger, + } + got, _, err := b.CreateStorage(tt.args.ctx, tt.args.bs) + if (err != nil) != tt.wantErr { + t.Errorf("CreateStorage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CreateStorage() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestBlobStorageProvider_GetReconcileTime(t *testing.T) { + type fields struct { + Client client.Client + Logger *logrus.Entry + } + type args struct { + bs *v1alpha1.BlobStorage + } + tests := []struct { + name string + fields fields + args args + want time.Duration + }{ + { + name: "test expected value for regression", + want: time.Second * 10, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := BlobStorageProvider{ + Client: tt.fields.Client, + Logger: tt.fields.Logger, + } + if got := b.GetReconcileTime(tt.args.bs); got != tt.want { + t.Errorf("GetReconcileTime() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestBlobStorageProvider_SupportsStrategy(t *testing.T) { + type fields struct { + Client client.Client + Logger *logrus.Entry + } + type args struct { + s string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "test success", + args: args{s: "openshift"}, + want: true, + }, + { + name: "test failure", + args: args{s: "test"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := BlobStorageProvider{ + Client: tt.fields.Client, + Logger: tt.fields.Logger, + } + if got := b.SupportsStrategy(tt.args.s); got != tt.want { + t.Errorf("SupportsStrategy() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/providers/openshift/provider_smtpcredentialset.go b/pkg/providers/openshift/provider_smtpcredentialset.go new file mode 100644 index 000000000..466314064 --- /dev/null +++ b/pkg/providers/openshift/provider_smtpcredentialset.go @@ -0,0 +1,96 @@ +package openshift + +import ( + "context" + + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1/types" + + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1" + + "strconv" + "time" + + "github.com/integr8ly/cloud-resource-operator/pkg/providers" + "github.com/integr8ly/cloud-resource-operator/pkg/providers/aws" + "github.com/integr8ly/cloud-resource-operator/pkg/resources" + "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + smtpPortPlaceholder = 587 + smtpTLSPlaceholder = true +) + +var _ providers.SMTPCredentialsProvider = (*SMTPCredentialProvider)(nil) + +type SMTPCredentialProvider struct { + Client client.Client + Logger *logrus.Entry +} + +func NewSMTPCredentialSetProvider(c client.Client, l *logrus.Entry) *SMTPCredentialProvider { + return &SMTPCredentialProvider{ + Client: c, + Logger: l, + } +} + +func (s SMTPCredentialProvider) GetName() string { + return "openshift-smtp" +} + +func (s SMTPCredentialProvider) SupportsStrategy(str string) bool { + return providers.OpenShiftDeploymentStrategy == str +} + +func (s SMTPCredentialProvider) GetReconcileTime(smtpCreds *v1alpha1.SMTPCredentialSet) time.Duration { + return time.Second * 10 +} + +func (s SMTPCredentialProvider) CreateSMTPCredentials(ctx context.Context, smtpCreds *v1alpha1.SMTPCredentialSet) (*providers.SMTPCredentialSetInstance, types.StatusMessage, error) { + dd := &aws.SMTPCredentialSetDetails{ + Username: varPlaceholder, + Password: varPlaceholder, + Port: smtpPortPlaceholder, + Host: varPlaceholder, + TLS: smtpTLSPlaceholder, + } + + if smtpCreds.Spec.SecretRef.Namespace == "" { + smtpCreds.Spec.SecretRef.Namespace = smtpCreds.Namespace + } + + if smtpCreds.Status.Phase != types.PhaseComplete || smtpCreds.Status.SecretRef.Name == "" || smtpCreds.Status.SecretRef.Namespace == "" { + return &providers.SMTPCredentialSetInstance{ + DeploymentDetails: dd, + }, "reconcile complete", nil + } + sec := &v1.Secret{} + if err := s.Client.Get(ctx, client.ObjectKey{Name: smtpCreds.Status.SecretRef.Name, Namespace: smtpCreds.Status.SecretRef.Namespace}, sec); err != nil { + return nil, "failed to reconcile", err + } + dd.Host = resources.StringOrDefault(string(sec.Data[aws.DetailsSMTPHostKey]), varPlaceholder) + dd.Password = resources.StringOrDefault(string(sec.Data[aws.DetailsSMTPPasswordKey]), varPlaceholder) + dd.Username = resources.StringOrDefault(string(sec.Data[aws.DetailsSMTPUsernameKey]), varPlaceholder) + ddPortStr := resources.StringOrDefault(string(sec.Data[aws.DetailsSMTPPortKey]), strconv.Itoa(smtpPortPlaceholder)) + ddPort, err := strconv.Atoi(ddPortStr) + if err != nil { + ddPort = smtpPortPlaceholder + } + ddTLSStr := resources.StringOrDefault(string(sec.Data[aws.DetailsSMTPTLSKey]), strconv.FormatBool(smtpTLSPlaceholder)) + ddTLS, err := strconv.ParseBool(ddTLSStr) + if err != nil { + ddTLS = smtpTLSPlaceholder + } + dd.Port = ddPort + dd.TLS = ddTLS + return &providers.SMTPCredentialSetInstance{ + DeploymentDetails: dd, + }, "reconcile complete", nil +} + +func (s SMTPCredentialProvider) DeleteSMTPCredentials(ctx context.Context, smtpCreds *v1alpha1.SMTPCredentialSet) (types.StatusMessage, error) { + return "deletion complete", nil +} diff --git a/pkg/providers/openshift/provider_smtpcredentialset_test.go b/pkg/providers/openshift/provider_smtpcredentialset_test.go new file mode 100644 index 000000000..ea8a9e56b --- /dev/null +++ b/pkg/providers/openshift/provider_smtpcredentialset_test.go @@ -0,0 +1,252 @@ +package openshift + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1" + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1/types" + "github.com/integr8ly/cloud-resource-operator/pkg/providers" + "github.com/integr8ly/cloud-resource-operator/pkg/providers/aws" + "github.com/sirupsen/logrus" + v12 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestSMTPCredentialProvider_CreateSMTPCredentials(t *testing.T) { + type fields struct { + Client client.Client + Logger *logrus.Entry + } + type args struct { + ctx context.Context + smtpCreds *v1alpha1.SMTPCredentialSet + } + tests := []struct { + name string + fields fields + args args + want *providers.SMTPCredentialSetInstance + wantErr bool + }{ + { + name: "test placeholders used when secret does not exist", + fields: fields{ + Client: fake.NewFakeClient(), + Logger: &logrus.Entry{}, + }, + args: args{ + ctx: context.TODO(), + smtpCreds: &v1alpha1.SMTPCredentialSet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: v1alpha1.SMTPCredentialSetSpec{ + SecretRef: &types.SecretRef{ + Name: "test-sec", + Namespace: "", + }, + }, + }, + }, + want: &providers.SMTPCredentialSetInstance{ + DeploymentDetails: &aws.SMTPCredentialSetDetails{ + Username: varPlaceholder, + Password: varPlaceholder, + Port: smtpPortPlaceholder, + Host: varPlaceholder, + TLS: smtpTLSPlaceholder, + }, + }, + wantErr: false, + }, + { + name: "test existing secret values are used if exist", + fields: fields{ + Client: fake.NewFakeClient(&v12.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Data: map[string][]byte{ + aws.DetailsSMTPUsernameKey: []byte("test"), + aws.DetailsSMTPPasswordKey: []byte("test"), + aws.DetailsSMTPHostKey: []byte("test"), + aws.DetailsSMTPTLSKey: []byte("false"), + aws.DetailsSMTPPortKey: []byte("123"), + }, + }), + Logger: &logrus.Entry{}, + }, + args: args{ + ctx: context.TODO(), + smtpCreds: &v1alpha1.SMTPCredentialSet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: v1alpha1.SMTPCredentialSetSpec{ + SecretRef: &types.SecretRef{ + Name: "test-sec", + Namespace: "", + }, + }, + Status: v1alpha1.SMTPCredentialSetStatus{ + Phase: types.PhaseComplete, + SecretRef: &types.SecretRef{ + Name: "test", + Namespace: "test", + }, + }, + }, + }, + want: &providers.SMTPCredentialSetInstance{ + DeploymentDetails: &aws.SMTPCredentialSetDetails{ + Username: "test", + Password: "test", + Port: 123, + Host: "test", + TLS: false, + }, + }, + wantErr: false, + }, + { + name: "test missing values are replaced with placeholders", + fields: fields{ + Client: fake.NewFakeClient(&v12.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Data: map[string][]byte{ + aws.DetailsSMTPPortKey: []byte("123"), + }, + }), Logger: &logrus.Entry{}, + }, + args: args{ + ctx: context.TODO(), + smtpCreds: &v1alpha1.SMTPCredentialSet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: v1alpha1.SMTPCredentialSetSpec{ + SecretRef: &types.SecretRef{ + Name: "test-sec", + Namespace: "", + }, + }, + Status: v1alpha1.SMTPCredentialSetStatus{ + Phase: types.PhaseComplete, + SecretRef: &types.SecretRef{ + Name: "test", + Namespace: "test", + }, + }, + }, + }, + want: &providers.SMTPCredentialSetInstance{ + DeploymentDetails: &aws.SMTPCredentialSetDetails{ + Username: varPlaceholder, + Password: varPlaceholder, + Port: 123, + Host: varPlaceholder, + TLS: smtpTLSPlaceholder, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := SMTPCredentialProvider{ + Client: tt.fields.Client, + Logger: tt.fields.Logger, + } + got, _, err := s.CreateSMTPCredentials(tt.args.ctx, tt.args.smtpCreds) + if (err != nil) != tt.wantErr { + t.Errorf("CreateSMTPCredentials() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CreateSMTPCredentials() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSMTPCredentialProvider_GetReconcileTime(t *testing.T) { + type fields struct { + Client client.Client + Logger *logrus.Entry + } + type args struct { + smtpCreds *v1alpha1.SMTPCredentialSet + } + tests := []struct { + name string + fields fields + args args + want time.Duration + }{ + { + name: "test expected time for regression", + want: time.Second * 10, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := SMTPCredentialProvider{ + Client: tt.fields.Client, + Logger: tt.fields.Logger, + } + if got := s.GetReconcileTime(tt.args.smtpCreds); got != tt.want { + t.Errorf("GetReconcileTime() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSMTPCredentialProvider_SupportsStrategy(t *testing.T) { + type fields struct { + Client client.Client + Logger *logrus.Entry + } + type args struct { + str string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "test supported", + args: args{str: "openshift"}, + want: true, + }, + { + name: "test unsupported", + args: args{str: "test"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := SMTPCredentialProvider{ + Client: tt.fields.Client, + Logger: tt.fields.Logger, + } + if got := s.SupportsStrategy(tt.args.str); got != tt.want { + t.Errorf("SupportsStrategy() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/resources/strings.go b/pkg/resources/strings.go index 808b0a90f..ef43de63b 100644 --- a/pkg/resources/strings.go +++ b/pkg/resources/strings.go @@ -48,3 +48,10 @@ func buildAlphanumRegexp() (*regexp.Regexp, error) { } return anReg, nil } + +func StringOrDefault(str, defaultTo string) string { + if str == "" { + return defaultTo + } + return str +} diff --git a/pkg/resources/strings_test.go b/pkg/resources/strings_test.go index 4db3320eb..309c1bd0a 100644 --- a/pkg/resources/strings_test.go +++ b/pkg/resources/strings_test.go @@ -62,3 +62,39 @@ func TestShortenString(t *testing.T) { }) } } + +func TestStringOrDefault(t *testing.T) { + type args struct { + str string + defaultTo string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "test default is returned", + args: args{ + str: "", + defaultTo: "def", + }, + want: "def", + }, + { + name: "test value is returned", + args: args{ + str: "test", + defaultTo: "def", + }, + want: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := StringOrDefault(tt.args.str, tt.args.defaultTo); got != tt.want { + t.Errorf("StringOrDefault() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/test/e2e/cro_test.go b/test/e2e/cro_test.go index 97e16d9fd..7f9f02727 100644 --- a/test/e2e/cro_test.go +++ b/test/e2e/cro_test.go @@ -10,8 +10,10 @@ import ( ) const ( - postgresName = "example-postgres" - redisName = "example-redis" + postgresName = "example-postgres" + redisName = "example-redis" + blobstorageName = "example-blobstorage" + smtpName = "example-smtp" ) var ( @@ -32,6 +34,18 @@ func TestCRO(t *testing.T) { t.Fatalf("failed to add Postgres custom resource scheme to framework: %v", err) } + // adding blob storage scheme to framework + blobstorageList := &v1alpha1.BlobStorage{} + if err := framework.AddToFrameworkScheme(apis.AddToScheme, blobstorageList); err != nil { + t.Fatalf("failed to add Blobstorage custom resource scheme to framework: %v", err) + } + + // adding smtp scheme to framework + smtpList := &v1alpha1.SMTPCredentialSet{} + if err := framework.AddToFrameworkScheme(apis.AddToScheme, smtpList); err != nil { + t.Fatalf("failed to add SMTP custom resource scheme to framework: %v", err) + } + // run subtests t.Run("cro-openshift-postgres-test", func(t *testing.T) { t.Run("Cluster", OpenshiftPostgresTestCluster) @@ -41,6 +55,14 @@ func TestCRO(t *testing.T) { t.Run("Cluster", OpenshiftRedisTestCluster) }) + t.Run("cro-openshift-blobstorage-test", func(t *testing.T) { + t.Run("Cluster", OpenshiftBlobstorageTestCluster) + }) + + t.Run("cro-openshift-smtp-test", func(t *testing.T) { + t.Run("Cluster", OpenshiftSMTPTestCluster) + }) + } // setup openshift postgres test env and executes subtests @@ -143,6 +165,42 @@ func OpenshiftRedisTestCluster(t *testing.T) { } } +func OpenshiftBlobstorageTestCluster(t *testing.T) { + t.Parallel() + ctx := framework.NewTestCtx(t) + defer ctx.Cleanup() + err := ctx.InitializeClusterResources(getCleanupOptions(t)) + if err != nil { + t.Fatalf("failed to initialize cluster resources: %v", err) + } + t.Log("initialized cluster resources") + + f := framework.Global + + // run blobstorage test + if err = OpenshiftBlobstorageBasicTest(t, f, *ctx); err != nil { + t.Fatal(err) + } +} + +func OpenshiftSMTPTestCluster(t *testing.T) { + t.Parallel() + ctx := framework.NewTestCtx(t) + defer ctx.Cleanup() + err := ctx.InitializeClusterResources(getCleanupOptions(t)) + if err != nil { + t.Fatalf("failed to initialize cluster resources: %v", err) + } + t.Log("initialized cluster resources") + + f := framework.Global + + // run smtp test + if err = OpenshiftSMTPBasicTest(t, f, *ctx); err != nil { + t.Fatal(err) + } +} + // returns cleanup options func getCleanupOptions(t *testing.T) *framework.CleanupOptions { return &framework.CleanupOptions{ diff --git a/test/e2e/openshift_blobstorage_test.go b/test/e2e/openshift_blobstorage_test.go new file mode 100644 index 000000000..64a4ffd0b --- /dev/null +++ b/test/e2e/openshift_blobstorage_test.go @@ -0,0 +1,121 @@ +package e2e + +import ( + goctx "context" + "fmt" + "testing" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1" + + framework "github.com/operator-framework/operator-sdk/pkg/test" + + errorUtil "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func OpenshiftBlobstorageBasicTest(t *testing.T, f *framework.Framework, ctx framework.TestCtx) error { + testBlobstorage, namespace, err := getBasicBlobstorage(ctx) + if err != nil { + return errorUtil.Wrapf(err, "failed to get blobstorage") + } + + // verify blobstorage create + if err := blobstorageCreateTest(t, f, testBlobstorage, namespace); err != nil { + return errorUtil.Wrapf(err, "create blobstorage test failure") + } + + // verify blobstorage delete + if err := blobstorageDeleteTest(t, f, testBlobstorage, namespace); err != nil { + return errorUtil.Wrapf(err, "delete blobstorage test failure") + } + + t.Logf("blobstorage basic test pass") + return nil +} + +// creates blobstorage resource, verifies everything is as expected +func blobstorageCreateTest(t *testing.T, f *framework.Framework, testBlobstorage *v1alpha1.BlobStorage, namespace string) error { + // create blobstorage resource + if err := f.Client.Create(goctx.TODO(), testBlobstorage, getCleanupOptions(t)); err != nil { + return errorUtil.Wrapf(err, "could not create example blobstorage") + } + t.Logf("created %s resource", testBlobstorage.Name) + + // poll cr for complete status phase + bcr := &v1alpha1.BlobStorage{} + err := wait.Poll(retryInterval, timeout, func() (done bool, err error) { + if err := f.Client.Get(goctx.TODO(), types.NamespacedName{Namespace: namespace, Name: blobstorageName}, bcr); err != nil { + return true, errorUtil.Wrapf(err, "could not get blobstorage cr") + } + if bcr.Status.Phase == v1alpha1.StatusPhase("complete") { + return true, nil + } + return false, nil + }) + if err != nil { + return err + } + t.Logf("blobstorage status phase %s", bcr.Status.Phase) + + // get created secret + sec := v1.Secret{} + if err := f.Client.Get(goctx.TODO(), types.NamespacedName{Namespace: bcr.Status.SecretRef.Namespace, Name: bcr.Status.SecretRef.Name}, &sec); err != nil { + return errorUtil.Wrapf(err, "could not get secret") + } + + // check for expected key values + for _, k := range []string{"bucketName", "bucketRegion", "credentialKeyID", "credentialSecretKey"} { + if sec.Data[k] == nil { + return errorUtil.New(fmt.Sprintf("secret %s value not found", k)) + } + } + + t.Logf("%s secret created successfully", bcr.Status.SecretRef.Name) + return nil +} + +// removes blobstorage resource and verifies all components have been cleaned up +func blobstorageDeleteTest(t *testing.T, f *framework.Framework, testBlobstorage *v1alpha1.BlobStorage, namespace string) error { + // delete blobstorage resource + if err := f.Client.Delete(goctx.TODO(), testBlobstorage); err != nil { + return errorUtil.Wrapf(err, "failed to delete example blobstorage") + } + + sec := v1.Secret{} + if err := f.Client.Get(goctx.TODO(), types.NamespacedName{Namespace: namespace, Name: testBlobstorage.Spec.SecretRef.Name}, &sec); err != nil { + if errors.IsNotFound(err) { + return nil + } + return errorUtil.Wrapf(err, "could not get secret") + } + t.Logf("%s custom resource deleted", testBlobstorage.Name) + + return nil +} + +func getBasicBlobstorage(ctx framework.TestCtx) (*v1alpha1.BlobStorage, string, error) { + namespace, err := ctx.GetNamespace() + if err != nil { + return nil, "", errorUtil.Wrapf(err, "could not get namespace") + } + + return &v1alpha1.BlobStorage{ + ObjectMeta: metav1.ObjectMeta{ + Name: blobstorageName, + Namespace: namespace, + }, + Spec: v1alpha1.BlobStorageSpec{ + SecretRef: &v1alpha1.SecretRef{ + Name: "example-blobstorage-sec", + Namespace: namespace, + }, + Tier: "development", + Type: "workshop", + }, + }, namespace, nil +} diff --git a/test/e2e/openshift_smtp_test.go b/test/e2e/openshift_smtp_test.go new file mode 100644 index 000000000..9f0d5726b --- /dev/null +++ b/test/e2e/openshift_smtp_test.go @@ -0,0 +1,122 @@ +package e2e + +import ( + goctx "context" + "fmt" + "testing" + + "k8s.io/apimachinery/pkg/api/errors" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + + "github.com/integr8ly/cloud-resource-operator/pkg/apis/integreatly/v1alpha1" + + framework "github.com/operator-framework/operator-sdk/pkg/test" + + errorUtil "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func OpenshiftSMTPBasicTest(t *testing.T, f *framework.Framework, ctx framework.TestCtx) error { + testSMTP, namespace, err := getBasicSMTP(ctx) + if err != nil { + return errorUtil.Wrapf(err, "failed to get smtp") + } + + // verify smtp create + if err := smtpCreateTest(t, f, testSMTP, namespace); err != nil { + return errorUtil.Wrapf(err, "create smtp test failure") + } + + // verify smtp delete + if err := smtpDeleteTest(t, f, testSMTP, namespace); err != nil { + return errorUtil.Wrapf(err, "delete smtp test failure") + } + + t.Logf("smtp basic test pass") + return nil +} + +// creates smtp resource, verifies everything is as expected +func smtpCreateTest(t *testing.T, f *framework.Framework, testSMTP *v1alpha1.SMTPCredentialSet, namespace string) error { + // create smtp resource + if err := f.Client.Create(goctx.TODO(), testSMTP, getCleanupOptions(t)); err != nil { + return errorUtil.Wrapf(err, "could not create example smtp") + } + t.Logf("created %s resource", testSMTP.Name) + + // poll cr for complete status phase + scr := &v1alpha1.SMTPCredentialSet{} + err := wait.Poll(retryInterval, timeout, func() (done bool, err error) { + if err := f.Client.Get(goctx.TODO(), types.NamespacedName{Namespace: namespace, Name: smtpName}, scr); err != nil { + return true, errorUtil.Wrapf(err, "could not get smtp cr") + } + if scr.Status.Phase == v1alpha1.StatusPhase("complete") { + return true, nil + } + return false, nil + }) + if err != nil { + return err + } + t.Logf("smtp status phase %s", scr.Status.Phase) + + // get created secret + sec := v1.Secret{} + if err := f.Client.Get(goctx.TODO(), types.NamespacedName{Namespace: scr.Status.SecretRef.Namespace, Name: scr.Status.SecretRef.Name}, &sec); err != nil { + return errorUtil.Wrapf(err, "could not get secret") + } + + // check for expected key values + for _, k := range []string{"username", "password", "port", "host", "tls"} { + if sec.Data[k] == nil { + return errorUtil.New(fmt.Sprintf("secret %s value not found", k)) + } + } + + t.Logf("%s secret created successfully", scr.Status.SecretRef.Name) + return nil +} + +// removes smtp resource and verifies all components have been cleaned up +func smtpDeleteTest(t *testing.T, f *framework.Framework, testSMTP *v1alpha1.SMTPCredentialSet, namespace string) error { + // delete smtp resource + if err := f.Client.Delete(goctx.TODO(), testSMTP); err != nil { + return errorUtil.Wrapf(err, "failed to delete example smtp") + } + t.Logf("%s custom resource deleted", testSMTP.Name) + + sec := v1.Secret{} + if err := f.Client.Get(goctx.TODO(), types.NamespacedName{Namespace: namespace, Name: testSMTP.Spec.SecretRef.Name}, &sec); err != nil { + if errors.IsNotFound(err) { + return nil + } + return errorUtil.Wrapf(err, "could not get secret") + } + t.Logf("") + return nil +} + +func getBasicSMTP(ctx framework.TestCtx) (*v1alpha1.SMTPCredentialSet, string, error) { + namespace, err := ctx.GetNamespace() + if err != nil { + return nil, "", errorUtil.Wrapf(err, "could not get namespace") + } + + return &v1alpha1.SMTPCredentialSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: smtpName, + Namespace: namespace, + }, + Spec: v1alpha1.SMTPCredentialSetSpec{ + SecretRef: &v1alpha1.SecretRef{ + Name: "example-smtp-sec", + Namespace: namespace, + }, + Tier: "development", + Type: "workshop", + }, + }, namespace, nil +}