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

Update /xh endpoints and config for version/instance change detection #387

Merged
merged 12 commits into from
Sep 3, 2024
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
* `ReplicatedValue` has been replaced with the enhanced `CachedValue`. This new object provides
all the functionality of the old, plus additional features from the `Cache` API such as expiry,
`getOrCreate()`, event support, and blocking support for non-primary nodes.
* Migrated previous `xhAppVersionCheck` to new `xhEnvPollConfig`, which now governs a single polling
interval on the client to check for app version and connected instance changes. The previous
config's `mode` value will be automatically migrated to the new `onVersionChange` key. A shorter
default interval of 10s will be set in all cases, to ensure timely detection of instance changes.
* The `/xh/environment` endpoint is no longer whitelisted and requires / will trigger
authentication flow.

### 🎁 New Features

Expand Down
31 changes: 0 additions & 31 deletions grails-app/controllers/io/xh/hoist/PingController.groovy

This file was deleted.

2 changes: 1 addition & 1 deletion grails-app/controllers/io/xh/hoist/UrlMappings.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class UrlMappings {
"/$controller/$action?/$id?(.$format)?"{}

"404" (controller: 'xh', action: 'notFound')
"/ping" (controller: 'xh', action: 'version')

//------------------------
// Rest Support
Expand All @@ -29,7 +30,6 @@ class UrlMappings {
action = [POST: 'create', GET: 'read', PUT: 'update', DELETE: 'delete']
}


//------------------------
// Proxy Support
//------------------------
Expand Down
45 changes: 29 additions & 16 deletions grails-app/controllers/io/xh/hoist/impl/XhController.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,20 @@ class XhController extends BaseController {
BaseUserService userService
ClusterService clusterService


//------------------------
// Identity / Auth
//------------------------
def authStatus() {
renderJSON(authenticated: authUser != null)
}

/** Whitelisted endpoint to return auth-related settings for client bootstrap. */
def authConfig() {
def svc = Utils.appContext.getBean(BaseAuthenticationService)
renderJSON(svc.clientConfig)
}

def getIdentity() {
renderJSON(identityService.clientConfig)
}
Expand Down Expand Up @@ -170,6 +177,7 @@ class XhController extends BaseController {
renderJSON(success: true)
}


//------------------------
// Json Blobs
//------------------------
Expand Down Expand Up @@ -219,16 +227,11 @@ class XhController extends BaseController {
renderJSON(environmentService.getEnvironment())
}

def version() {
amcclain marked this conversation as resolved.
Show resolved Hide resolved
def options = configService.getMap('xhAppVersionCheck', [:])
renderJSON(
*: options,
instanceName: clusterService.instanceName,
appVersion: Utils.appVersion,
appBuild: Utils.appBuild
)
def environmentPoll() {
renderJSON(environmentService.environmentPoll())
}


//------------------------
// Client Errors
//------------------------
Expand All @@ -246,6 +249,8 @@ class XhController extends BaseController {
renderJSON(success: true)
}



//------------------------
// Feedback
//------------------------
Expand All @@ -258,16 +263,32 @@ class XhController extends BaseController {
renderJSON(success: true)
}


//----------------------
// Alert Banner
//----------------------
def alertBanner() {
renderJSON(alertBannerService.alertBanner)
}


//-----------------------
// Misc
//-----------------------
/**
* Whitelisted (pre-auth) endpoint with minimal app identifier and version info.
* Also reachable (for backwards compatibility) via /ping, as per `UrlMappings`.
*/
def version() {
renderJSON(
appCode: Utils.appCode,
appVersion: Utils.appVersion,
appBuild: Utils.appBuild,
timestamp: System.currentTimeMillis(),
success: true
)
}

/**
* Returns the timezone offset for a given timezone ID.
* While abbrevs (e.g. 'GMT', 'PST', 'UTC+04') are supported, fully qualified IDs (e.g.
Expand All @@ -283,14 +304,6 @@ class XhController extends BaseController {
renderJSON([offset: tz.getOffset(System.currentTimeMillis())])
}

/**
* Auth-related settings for the client. Accessible pre-auth via whitelist.
*/
def authConfig() {
def svc = Utils.appContext.getBean(BaseAuthenticationService)
renderJSON(svc.clientConfig)
}

/**
* Utility to echo all headers received on the request. Useful in particular for verifying
* headers (e.g. `jespa_connection_id`) that are installed by or must pass through multiple
Expand Down
29 changes: 14 additions & 15 deletions grails-app/init/io/xh/hoist/BootStrap.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -99,21 +99,6 @@ class BootStrap implements LogSupport {
groupName: 'xh.io',
note: 'Official TimeZone for this application - e.g. the zone of the head office. Used to format/parse business related dates that need to be considered and displayed consistently at all locations. Set to a valid Java TimeZone ID.'
],
xhAppVersionCheck: [
valueType: 'json',
defaultValue: [
interval: configService.getInt('xhAppVersionCheckSecs', 30),
mode: configService.getBool('xhAppVersionCheckEnabled', true) ? 'promptReload' : 'silent'
],
clientVisible: true,
groupName: 'xh.io',
note: "Controls application behaviour when the server reports to the client that a new version is available. Supports the following options:\n\n" +
"-'interval': Frequency (in seconds) with which the version of the app should be checked. Value of -1 disables version checking.\n" +
"-'mode': Action taken by client upon a new version becoming available. Supports the following options:\n" +
"\t+ 'forceReload': Force clients to refresh immediately. To be used when an updated server is known to be incompatible with a previously deployed client.\n" +
"\t+ 'promptReload': Show an update prompt banner, allowing users to refresh when convenient.\n" +
"\t+ 'silent': No action taken."
],
xhAutoRefreshIntervals: [
valueType: 'json',
defaultValue: [app: -1],
Expand Down Expand Up @@ -197,6 +182,20 @@ class BootStrap implements LogSupport {
groupName: 'xh.io',
note: 'True to enable the monitor tab included with the Hoist Admin console and the associated server-side jobs'
],
xhEnvPollConfig: [
valueType: 'json',
defaultValue: [
interval: 10,
onVersionChange: configService.getMap('xhAppVersionCheck', [mode: 'promptReload']).get('mode')
],
groupName: 'xh.io',
note: "Controls client calls to server to poll for version, instance changes, or auth changes. Supports the following options:\n\n" +
"- interval: Frequency (in seconds) with which the status of the app server should be polled. Value of -1 disables checking.\n" +
"- onVersionChange: Action taken by client upon a new version becoming available, one of:\n" +
"\t+ 'forceReload': Force clients to refresh immediately. To be used when an updated server is known to be incompatible with a previously deployed client.\n" +
"\t+ 'promptReload': Show an update prompt banner, allowing users to refresh when convenient.\n" +
"\t+ 'silent': No action taken."
],
xhExpectedServerTimeZone: [
valueType: 'string',
defaultValue: '*',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import grails.plugins.GrailsPlugin
import grails.util.GrailsUtil
import grails.util.Holders
import io.xh.hoist.BaseService
import io.xh.hoist.config.ConfigService
import io.xh.hoist.util.Utils
import io.xh.hoist.websocket.WebSocketService


/**
Expand All @@ -20,16 +22,13 @@ import io.xh.hoist.util.Utils
*/
class EnvironmentService extends BaseService {

def configService,
webSocketService
ConfigService configService
WebSocketService webSocketService

private TimeZone _appTimeZone
private Map _pollResult

static clearCachesConfigs = ['xhAppTimeZone']

void init() {
_appTimeZone = calcAppTimeZone()
}
static clearCachesConfigs = ['xhAppTimeZone', 'xhEnvPollConfig']

/**
* Official TimeZone for this application - e.g. the zone of the head office or trading center.
Expand All @@ -39,15 +38,15 @@ class EnvironmentService extends BaseService {
* Not to be confused with `serverTimeZone` below.
*/
TimeZone getAppTimeZone() {
return _appTimeZone
return _appTimeZone ?= calcAppTimeZone()
}

/** TimeZone of the server/JVM running this application. */
TimeZone getServerTimeZone() {
return Calendar.instance.timeZone
}

/** Bundle of environment-related metadata, for serialization to JS clients. */
/** Full bundle of environment-related metadata, for serialization to JS clients. */
Map getEnvironment() {
def serverTz = serverTimeZone,
appTz = appTimeZone,
Expand All @@ -66,23 +65,38 @@ class EnvironmentService extends BaseService {
appTimeZone: appTz.toZoneId().id,
appTimeZoneOffset: appTz.getOffset(now),
webSocketsEnabled: webSocketService.enabled,
instanceName: clusterService.instanceName
instanceName: clusterService.instanceName,
pollConfig: configService.getMap('xhEnvPollConfig')
]

hoistGrailsPlugins.each {it ->
ret[it.name + 'Version'] = it.version
}

def user = authUser
if (user?.isHoistAdminReader) {
if (authUser.isHoistAdminReader) {
def dataSource = Utils.dataSourceConfig
ret.databaseConnectionString = dataSource.url
ret.databaseUser = dataSource.username
ret.databaseCreateMode = dataSource.dbCreate
}

return ret
}

/**
* Report server version and instance identity to the client.
* Designed to be called frequently by client. Should be minimal and highly optimized.
*/
Map environmentPoll() {
return _pollResult ?= [
appCode : Utils.appCode,
appVersion : Utils.appVersion,
appBuild : Utils.appBuild,
instanceName: clusterService.instanceName,
pollConfig : configService.getMap('xhEnvPollConfig'),
]
}


//---------------------
// Implementation
Expand All @@ -104,7 +118,8 @@ class EnvironmentService extends BaseService {
}

void clearCaches() {
this._appTimeZone = calcAppTimeZone()
_appTimeZone = null
_pollResult = null
super.clearCaches()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ abstract class BaseAuthenticationService extends BaseService {
'/ping',
'/xh/login',
'/xh/logout',
'/xh/environment',
'/xh/version',
'/xh/authConfig'
]
Expand Down
Loading