Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable binding of postgres CF db service to CF hosted console #1231

Merged
merged 14 commits into from
Sep 13, 2017
Merged
4 changes: 4 additions & 0 deletions .cfignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ node_modules/
bower_components/
dist/
components/*/backend/vendor
dev-certs/
out/
outputs/
tmp/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ deploy/development.rc
deploy/ci/secrets.yml
deploy/kubernetes/values.yaml
outputs/
deploy/cloud-foundry/db-migration/goose
10 changes: 4 additions & 6 deletions components/app-core/backend/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestLoginToUAA(t *testing.T) {
WillReturnRows(expectNoRows())

mock.ExpectExec(insertIntoTokens).
// WithArgs(mockUserGUID, "uaa", mockTokenRecord.AuthToken, mockTokenRecord.RefreshToken, newExpiry).
// WithArgs(mockUserGUID, "uaa", mockTokenRecord.AuthToken, mockTokenRecord.RefreshToken, newExpiry).
WillReturnResult(sqlmock.NewResult(1, 1))

Convey("Should not fail to login", func() {
Expand Down Expand Up @@ -88,7 +88,6 @@ func TestLoginToUAAWithBadCreds(t *testing.T) {
pp.Config.ConsoleConfig.UAAEndpoint = uaaUrl
pp.Config.ConsoleConfig.SkipSSLValidation = true


err := pp.loginToUAA(ctx)
Convey("Login to UAA should fail", func() {
So(err, ShouldNotBeNil)
Expand Down Expand Up @@ -129,9 +128,8 @@ func TestLoginToUAAButCantSaveToken(t *testing.T) {
pp.Config.ConsoleConfig.UAAEndpoint = uaaUrl
pp.Config.ConsoleConfig.SkipSSLValidation = true


mock.ExpectQuery(selectAnyFromTokens).
// WithArgs(mockUserGUID).
// WithArgs(mockUserGUID).
WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow("0"))

// --- set up the database expectation for pp.saveUAAToken
Expand Down Expand Up @@ -207,7 +205,7 @@ func TestLoginToCNSI(t *testing.T) {
// Setup expectation that the CNSI token will get saved
//encryptedUAAToken, _ := tokens.EncryptToken(pp.Config.EncryptionKeyInBytes, mockUAAToken)
mock.ExpectExec(insertIntoTokens).
//WithArgs(mockCNSIGUID, mockUserGUID, "cnsi", encryptedUAAToken, encryptedUAAToken, sessionValues["exp"]).
//WithArgs(mockCNSIGUID, mockUserGUID, "cnsi", encryptedUAAToken, encryptedUAAToken, sessionValues["exp"]).
WillReturnResult(sqlmock.NewResult(1, 1))

// do the call
Expand Down Expand Up @@ -660,7 +658,7 @@ func TestVerifySessionExpired(t *testing.T) {

mock.ExpectQuery(selectAnyFromTokens).
WillReturnRows(sqlmock.NewRows([]string{"auth_token", "refresh_token", "token_expiry"}).
AddRow(mockUAAToken, mockUAAToken, sessionValues["exp"]))
AddRow(mockUAAToken, mockUAAToken, sessionValues["exp"]))
err := pp.verifySession(ctx)

Convey("Should fail to verify session", func() {
Expand Down
24 changes: 18 additions & 6 deletions components/app-core/backend/cnsi.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,19 +262,31 @@ func (p *portalProxy) GetCNSIRecord(guid string) (interfaces.CNSIRecord, error)
return rec, nil
}

func (p *portalProxy) cnsiRecordExists(endpoint string) bool {
log.Debug("cnsiRecordExists")
func (p *portalProxy) GetCNSIRecordByEndpoint(endpoint string) (interfaces.CNSIRecord, error) {
log.Debug("GetCNSIRecordByEndpoint")
var rec interfaces.CNSIRecord

cnsiRepo, err := cnsis.NewPostgresCNSIRepository(p.DatabaseConnectionPool)
if err != nil {
return false
return rec, err
}

_, err = cnsiRepo.FindByAPIEndpoint(endpoint)
rec, err = cnsiRepo.FindByAPIEndpoint(endpoint)
if err != nil {
return false
return rec, err
}

return true
// Ensure that trailing slash is removed from the API Endpoint
rec.APIEndpoint.Path = strings.TrimRight(rec.APIEndpoint.Path, "/")

return rec, nil
}

func (p *portalProxy) cnsiRecordExists(endpoint string) bool {
log.Debug("cnsiRecordExists")

_, err := p.GetCNSIRecordByEndpoint(endpoint);
return err == nil
}

func (p *portalProxy) setCNSIRecord(guid string, c interfaces.CNSIRecord) error {
Expand Down
68 changes: 68 additions & 0 deletions components/app-core/backend/datastore/database_cf_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package datastore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in the cloud-foundry-hosting component


import (
"encoding/json"
"github.com/SUSE/stratos-ui/components/app-core/backend/config"
log "github.com/Sirupsen/logrus"
"strconv"
"strings"
)

const (
SERVICES_ENV = "VCAP_SERVICES"
)

type VCAPService struct {
Credentials VCAPCredential `json:"credentials"`
Tags []string `json:"tags"`
}

type VCAPCredential struct {
Username string `json:"username"`
Password string `json:"password"`
Dbname string `json:"dbname"`
Hostname string `json:"hostname"`
Port string `json:"port"`
Uri string `json:"uri"`
}

// Discover cf db services via their 'uri' env var and apply settings to the DatabaseConfig objects
func ParseCFEnvs(db *DatabaseConfig) bool {
if config.IsSet(SERVICES_ENV) == false {
return false
}

// Extract struts from VCAP_SERVICES env
vcapServicesStr := config.GetString(SERVICES_ENV)
var vcapServices map[string][]VCAPService
err := json.Unmarshal([]byte(vcapServicesStr), &vcapServices)
if err != nil {
log.Warnf("Unable to convert %s env var into JSON", SERVICES_ENV)
return false
}

for _, services := range vcapServices {
if len(services) == 0 {
continue
}
service := services[0]

for _, tag := range service.Tags {
if strings.HasPrefix(tag, "stratos_postgresql") {
dbCredentials := service.Credentials
// At the moment we only handle Postgres
db.DatabaseProvider = "pgsql"
db.Username = dbCredentials.Username
db.Password = dbCredentials.Password
db.Database = dbCredentials.Dbname
db.Host = dbCredentials.Hostname
db.Port, err = strconv.Atoi(dbCredentials.Port)
db.SSLMode = "disable"
log.Info("Discovered Cloud Foundry postgres service and applied config")
return true
}
}
}

return false
}
5 changes: 4 additions & 1 deletion components/app-core/backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,10 @@ func loadPortalConfig(pc interfaces.PortalConfig) (interfaces.PortalConfig, erro

func loadDatabaseConfig(dc datastore.DatabaseConfig) (datastore.DatabaseConfig, error) {
log.Debug("loadDatabaseConfig")
if err := config.Load(&dc); err != nil {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a plugin extension point so that the CF-specific hosting code can go in the cloud foundry hosting component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a look and also talked to Irfan, it's not something we can do at the moment. This is called right at the beginning of the PP start up process before the portal proxy object and plugins are initialised. Fixing this would require a bit of a rework.

if datastore.ParseCFEnvs(&dc) == true {
log.Info("Using Cloud Foundry DB service")
} else if err := config.Load(&dc); err != nil {
return dc, fmt.Errorf("Unable to load database configuration. %v", err)
}

Expand Down
1 change: 0 additions & 1 deletion components/app-core/backend/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ func TestLoadPortalConfig(t *testing.T) {
t.Error("Unable to get TLSAddress from config")
}


if result.CFClient != "portal-proxy" {
t.Error("Unable to get CFClient from config")
}
Expand Down
27 changes: 13 additions & 14 deletions components/app-core/backend/mock_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,11 @@ func setupPortalProxy(db *sql.DB) *portalProxy {
urlP, _ := url.Parse("https://login.52.38.188.107.nip.io:50450")
pc := interfaces.PortalConfig{
ConsoleConfig: &interfaces.ConsoleConfig{
ConsoleClient: "console",
ConsoleClientSecret: "",
UAAEndpoint: urlP,
SkipSSLValidation: true,
ConsoleClient: "console",
ConsoleClientSecret: "",
UAAEndpoint: urlP,
SkipSSLValidation: true,
ConsoleAdminScope: UAAAdminIdentifier,

},
SessionStoreSecret: "hiddenraisinsohno!",
EncryptionKeyInBytes: mockEncryptionKey,
Expand Down Expand Up @@ -263,21 +262,21 @@ var mockUAAResponse = UAAResponse{
}

const (
mockAPIEndpoint = "https://api.127.0.0.1"
mockAuthEndpoint = "https://login.127.0.0.1"
mockTokenEndpoint = "https://uaa.127.0.0.1"
mockAPIEndpoint = "https://api.127.0.0.1"
mockAuthEndpoint = "https://login.127.0.0.1"
mockTokenEndpoint = "https://uaa.127.0.0.1"
mockDopplerEndpoint = "https://doppler.127.0.0.1"
mockProxyVersion = 20161117141922
mockProxyVersion = 20161117141922

stringCFType = "cf"
stringCEType = "hce"

selectAnyFromTokens = `SELECT .+ FROM tokens WHERE .+`
insertIntoTokens = `INSERT INTO tokens`
updateTokens = `UPDATE tokens`
selectAnyFromCNSIs = `SELECT (.+) FROM cnsis WHERE (.+)`
insertIntoCNSIs = `INSERT INTO cnsis`
getDbVersion = `SELECT version_id FROM goose_db_version WHERE is_applied = 't' ORDER BY id DESC LIMIT 1`
insertIntoTokens = `INSERT INTO tokens`
updateTokens = `UPDATE tokens`
selectAnyFromCNSIs = `SELECT (.+) FROM cnsis WHERE (.+)`
insertIntoCNSIs = `INSERT INTO cnsis`
getDbVersion = `SELECT version_id FROM goose_db_version WHERE is_applied = 't' ORDER BY id DESC LIMIT 1`
)

var rowFieldsForCNSI = []string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type PortalProxy interface {
DoLoginToCNSI(c echo.Context, cnsiGUID string) (*LoginRes, error)
// Expose internal portal proxy records to extensions
GetCNSIRecord(guid string) (CNSIRecord, error)
GetCNSIRecordByEndpoint(endpoint string) (CNSIRecord, error)
GetCNSITokenRecord(cnsiGUID string, userGUID string) (TokenRecord, bool)
GetCNSIUser(cnsiGUID string, userGUID string) (*ConnectedUser, bool)
GetConfig() *PortalConfig
Expand Down
32 changes: 21 additions & 11 deletions components/cloud-foundry-hosting/backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,28 @@ func (ch *CFHosting) Init() error {
return fmt.Errorf("Failed to save console configuration due to %s", err)
}

// Auto-register the Cloud Foundry
cfCnsi, regErr := ch.portalProxy.DoRegisterEndpoint("Cloud Foundry", appData.API, true, cfEndpointSpec.Info)
if regErr != nil {
log.Fatal("Could not auto-register the Cloud Foundry endpoint", err)
ch.portalProxy.GetConfig().CloudFoundryInfo = &interfaces.CFInfo{
SpaceGUID: appData.SpaceID,
AppGUID: appData.ApplicationID,
var cfCnsi interfaces.CNSIRecord

cfCnsi, err = ch.portalProxy.GetCNSIRecordByEndpoint(appData.API)
if err != nil {
return fmt.Errorf("Failed to discover if an endpoint for hosting cf exists due to %s", err)
} else if cfCnsi.CNSIType != "" {
log.Info("Found existing endpoint matching Cloud Foundry API. Will not auto-register or auto-connect")
} else {
log.Info("Auto-registering endpoint for Cloud Foundry API")
var regErr error
// Auto-register the Cloud Foundry
cfCnsi, regErr = ch.portalProxy.DoRegisterEndpoint("Cloud Foundry", appData.API, true, cfEndpointSpec.Info)
if regErr != nil {
log.Fatal("Could not auto-register the Cloud Foundry endpoint", err)
ch.portalProxy.GetConfig().CloudFoundryInfo = &interfaces.CFInfo{
SpaceGUID: appData.SpaceID,
AppGUID: appData.ApplicationID,
}
return nil
}
return nil
// Add login hook to automatically connect to the Cloud Foundry when the user logs in
ch.portalProxy.GetConfig().LoginHook = ch.cfLoginHook
}

// Store the space and id of the ConsocfLoginHookle application - we can use these to prevent stop/delete in the front-end
Expand All @@ -173,9 +186,6 @@ func (ch *CFHosting) Init() error {
EndpointGUID: cfCnsi.GUID,
}

// Add login hook to automatically conneect to the Cloud Foundry when the user logs in
ch.portalProxy.GetConfig().LoginHook = ch.cfLoginHook

log.Info("All done for Cloud Foundry deployment")
}
return nil
Expand Down
7 changes: 5 additions & 2 deletions deploy/cloud-foundry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ applications:

### Enable Endpoints Dashboard to register additional Cloud Foundry endpoints

>**NOTE** This method is meant to demonstrate the capabilities of the console with multiple endpoints and is not meant for production environments
>**NOTE** This feature, on it's own, is meant to demonstrate the capabilities of the console with multiple endpoints and is not meant for production environments.

This method comes with two caveats.
This method comes with two caveats. To remove these caveats see [here](#Associate-Cloud-Foundry-database-service).

1. The console will lose stored data when a cf app instance is restarted
2. Multiple instances of the app will contain multiple separate stored data instances. This will mean the user may connect to a different one with a different storage when revisiting the console.
Expand All @@ -162,3 +162,6 @@ applications:
env:
FORCE_ENDPOINT_DASHBOARD: true
```

### Associate Cloud Foundry database service
Follow instructions [here](db-migration/README.md).
35 changes: 35 additions & 0 deletions deploy/cloud-foundry/db-migration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Associated a Cloud Foundry database service
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be "Associate"


As mentioned in the standard cf push instructions [here]("../README.md") the console as deployed via cf push
does not contain any way to persist date over application restarts and db entries such as registered endpoints
and user tokens are lost. To resolve this a Cloud Foundry db service can be binded to the console. Run through
the steps below to implement.

> **NOTE** The console supports postgresql. Your Cloud Foundry deployment should contain a service for
the desired db tagged with 'postgresql'.

1. Enable the endpoint dashboard
* This is not strictly required, but at the moment is the only motivator to follow the next steps
* Add the following to the manifest
```
env:
FORCE_ENDPOINT_DASHBOARD: true
```
1. Create the console app and associated a postgres service instance
* Use the instructions [here]("../README.md")
* Log into the console and then navigate to the console application
* In the `Services` tab associate a new postgres service instance to the application
* Validate in the `Variables` tab that `VCAP_SERVICES` is populated with new postgres credentials
* Remember to update the manifest with the services section
1. Set up the associated db instance. Run the following from the root of the console
```
cf push -c "deploy/cloud-foundry/db-migration/db-migrate.sh" -u "process"
```
> **NOTE** All subsequent pushes, restarts, restaging will use this migration command.
It's therefore very important to execute the next step in order for the console to start
1. Restart the app via cf push
```
cf push -c "null"
```


39 changes: 39 additions & 0 deletions deploy/cloud-foundry/db-migration/db-migrate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash

set -e

echo "Attempting to migrating database"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad grammar - "Attempting to migrate database"


DB_MIGRATE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
DEPLOY_DIR=${DB_MIGRATE_DIR}/../../

export STRATOS_TEMP=$(mktemp -d)

export GOPATH=${DB_MIGRATE_DIR}/goose
export GOBIN=$GOPATH/bin
go get bitbucket.org/liamstask/goose/cmd/goose

export STRATOS_DB_ENV="$STRATOS_TEMP/db.env"
node ${DB_MIGRATE_DIR}/parse_db_environment.js $STRATOS_DB_ENV
source $STRATOS_DB_ENV

cd $DEPLOY_DIR
case $DB_TYPE in
"postgresql")
echo "Migrating postgresql instance on $DB_HOST"
$GOBIN/goose -env cf_postgres up
if [ $? -eq 0 ]; then
while
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its more common to put:

while true; do
echo "Database successfully migrated. Please restart the application via 'cf push -c "null"'";
sleep 60
done

echo "Database successfully migrated. Please restart the application via 'cf push -c \"null\"'";
sleep 60
do
:
done
else
echo Database migration failed
fi
;;
*)
echo Unknown DB type '$DB_TYPE'
;;
esac
Loading