From f9346c0625894c9f9672f46278fa359277323ac0 Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Mon, 9 Mar 2026 16:15:51 -0400 Subject: [PATCH 1/9] Add eureka registration client Signed-off-by: 1000TurquoisePogs --- CHANGELOG.md | 3 + bin/configure.sh | 20 ++ build/build_zss.sh | 1 + c/eurekaClient.c | 739 ++++++++++++++++++++++++++++++++++++++++ c/zss.c | 10 + defaults.yaml | 3 + h/eurekaClient.h | 136 ++++++++ h/zssLogging.h | 76 +++++ manifest.template.yaml | 6 +- schemas/zss-config.json | 19 ++ 10 files changed, 1008 insertions(+), 5 deletions(-) create mode 100644 c/eurekaClient.c create mode 100644 h/eurekaClient.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 45d40e5d9..6cf942846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to the ZSS package will be documented in this file. +## `3.5.0` +- Enhancement: ZSS can now use eureka registration to APIML, providing improved high availability over prior static registration. Static registration is still available via new property `components.zss.agent.mediationLayer.static: true` ([#???]()) + ## `3.4.0` - Bugfix: Fixed hostname to IP address lookup for "bind-test" program. [(#801)](https://github.com/zowe/zss/pull/801) diff --git a/bin/configure.sh b/bin/configure.sh index 61af36b10..e88d667c3 100755 --- a/bin/configure.sh +++ b/bin/configure.sh @@ -28,3 +28,23 @@ if [ "${ZWE_components_app_server_enabled}" != "true" ]; then _CEE_RUNOPTS="XPLINK(ON),HEAPPOOLS(OFF),HEAPPOOLS64(OFF)" ${ZWE_zowe_runtimeDirectory}/bin/utils/configmgr -script "${ZWE_zowe_runtimeDirectory}/components/zss/bin/plugins-init.js" fi +# Register ZSS as a static APIML service when components.zss.agent.mediationLayer.static is enabled. +# ZWE_components_zss_agent_mediationLayer_static defaults to false; only +# proceed when it is explicitly set to true. +if [ "${ZWE_components_zss_agent_mediationLayer_static}" = "true" ]; then + if [ -n "${ZWE_STATIC_DEFINITIONS_DIR}" ]; then + apiml_static_def="${ZWE_STATIC_DEFINITIONS_DIR}/zss.apiml_static_reg_yaml_template.${ZWE_CLI_PARAMETER_HA_INSTANCE}.yml" + apiml_static_src="${COMPONENT_HOME}/apiml-static-reg.yaml.template" + parsed_def=$( ( echo "cat <&1) + echo "${parsed_def}" > "${apiml_static_def}" + chmod 770 "${apiml_static_def}" + fi +else + if [ -n "${ZWE_STATIC_DEFINITIONS_DIR}" ]; then + apiml_static_def="${ZWE_STATIC_DEFINITIONS_DIR}/zss.apiml_static_reg_yaml_template.${ZWE_CLI_PARAMETER_HA_INSTANCE}.yml" + if [ -f "${apiml_static_def}" ]; then + rm -f "${apiml_static_def}" + fi + fi +fi + diff --git a/build/build_zss.sh b/build/build_zss.sh index 394123121..e52155eec 100755 --- a/build/build_zss.sh +++ b/build/build_zss.sh @@ -253,6 +253,7 @@ xlc \ ${ZSS}/c/passTicketService.c \ ${ZSS}/c/jwk.c \ ${ZSS}/c/safIdtService.c \ + ${ZSS}/c/eurekaClient.c \ ${GSKLIB} #then diff --git a/c/eurekaClient.c b/c/eurekaClient.c new file mode 100644 index 000000000..0c8f9346a --- /dev/null +++ b/c/eurekaClient.c @@ -0,0 +1,739 @@ +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ + +/* + * eurekaClient.c + * + * Implements Eureka (API ML Discovery) client registration for ZSS. + * + * Registration flow (runs inside a dedicated RLETask): + * + * 1. POST /eureka/apps/ -- initial service registration + * 2. PUT /eureka/apps// -- heartbeat every 30 s + * + * On receipt of HTTP 404 from a heartbeat the task automatically re-registers. + * On any other network error the task sleeps retryIntervalSecs and tries again. + * + * All strings sent over HTTP are native EBCDIC; httpClientSessionSetRequestBody + * is called with bodyIsNativeText=TRUE so that the library translates them to + * ASCII before writing to the socket. + */ + +#ifdef METTLE +#error Metal C is not supported +#endif + +#include +#include +#include +#include +#include +#include + +#include "zowetypes.h" +#include "alloc.h" +#include "utils.h" +#include "bpxnet.h" +#include "collections.h" +#include "socketmgmt.h" +#include "le.h" +#include "logging.h" +#include "scheduling.h" +#include "json.h" +#include "httpclient.h" +#include "configmgr.h" +#ifdef USE_ZOWE_TLS +#include "tls.h" +#endif +#include "zssLogging.h" +#include "zss.h" +#include "eurekaClient.h" + +/* ------------------------------------------------------------------------- + * IPv6 helpers + * ------------------------------------------------------------------------- */ + +/* Returns true when addr is an IPv6 address (contains a colon). */ +static bool isIPv6Address(const char *addr) { + return addr && strchr(addr, ':') != NULL; +} + +/* Returns a safeMalloc'd copy of addr with every ':' replaced by '_'. + * Used to produce a legal Eureka instanceId component from an IPv6 address. */ +static char *ipv6ColonsToUnderscores(const char *addr) { + int len = strlen(addr); + char *dst = (char *)safeMalloc(len + 1, "eureka_ipv6id"); + if (!dst) return NULL; + for (int i = 0; i <= len; i++) { + dst[i] = (addr[i] == ':') ? '_' : addr[i]; + } + return dst; +} + +/* ------------------------------------------------------------------------- + * Internal context held by the RLETask + * ------------------------------------------------------------------------- */ +typedef struct EurekaClientContext_tag { + EurekaClientSettings *settings; +} EurekaClientContext; + +/* ------------------------------------------------------------------------- + * Return-code constants used internally + * ------------------------------------------------------------------------- */ +#define EUREKA_RC_OK 0 +#define EUREKA_RC_CTX_ERROR 1 +#define EUREKA_RC_SESSION_ERROR 2 +#define EUREKA_RC_STAGE_ERROR 3 +#define EUREKA_RC_SEND_ERROR 4 +#define EUREKA_RC_RECV_ERROR 5 +#define EUREKA_RC_HTTP_ERROR 6 /* unexpected HTTP status code */ +#define EUREKA_RC_NOT_FOUND 7 /* HTTP 404 -- instance not registered */ + +/* ------------------------------------------------------------------------- + * Eureka REST API defaults + * ------------------------------------------------------------------------- */ +#define EUREKA_HEARTBEAT_INTERVAL_SECS 30 +#define EUREKA_RETRY_INTERVAL_SECS 30 +#define EUREKA_RECV_TIMEOUT_SECS 10 +#define EUREKA_APPS_PATH_PREFIX "/eureka/apps/" + +/* Size of the static buffer used to build the JSON registration body. + * Eureka bodies are typically well under 4 KB. */ +#define EUREKA_BODY_BUFSIZE 4096 + +/* ------------------------------------------------------------------------- + * Forward declarations + * ------------------------------------------------------------------------- */ +static int eurekaClientTaskMain(RLETask *task); +static int doRegistration(EurekaClientSettings *settings, int *httpStatusOut); +static int doHeartbeat(EurekaClientSettings *settings, int *httpStatusOut); +static int sendEurekaRequest(EurekaClientSettings *settings, + const char *method, + const char *urlPath, + const char *body, /* NULL for heartbeat */ + int bodyLen, + int *httpStatusOut); +static int buildBodyPath(EurekaClientSettings *settings, + char *buf, int bufLen); +static int buildInstancePath(EurekaClientSettings *settings, + char *buf, int bufLen); +static int buildRegistrationBody(EurekaClientSettings *settings, + char *buf, int bufLen); + + +/* ========================================================================= + * Public API + * ========================================================================= */ + +/* + * makeEurekaClientSettings + * + * Reads from the Zowe YAML: + * - zowe.externalDomains[0] -> discoveryHost / hostName for ZSS + * - components.discovery.port -> discoveryPort + * - zssPort (passed by caller) -> port + * + * Constructs the registration URL fragments from these values. + */ +EurekaClientSettings *makeEurekaClientSettings(ShortLivedHeap *slh, + ConfigManager *configmgr, + const char *zssAddress, + int zssPort, + bool zssSecure, + const char *zssVersion +#ifdef USE_ZOWE_TLS + , TlsEnvironment *tlsEnv +#endif + ) +{ + /* ---- discovery or apiml enabled? -------------------------------- */ + bool discoveryEnabled = false; + cfgGetBooleanC(configmgr, ZSS_CFGNAME, &discoveryEnabled, + 3, "components", "discovery", "enabled"); + + bool apimlEnabled = false; + cfgGetBooleanC(configmgr, ZSS_CFGNAME, &apimlEnabled, + 3, "components", "apiml", "enabled"); + + if (!discoveryEnabled && !apimlEnabled) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "Eureka registration skipped: neither components.discovery.enabled " + "nor components.apiml.enabled is true\n"); + return NULL; + } + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "Eureka registration enabled (discovery=%d, apiml=%d)\n", + discoveryEnabled, apimlEnabled); + + /* ---- discovery port -------------------------------------------- */ + int discoveryPort = 0; + int portStatus = cfgGetIntC(configmgr, ZSS_CFGNAME, &discoveryPort, + 3, "components", "discovery", "port"); + if (portStatus != ZCFG_SUCCESS || discoveryPort <= 0) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "Eureka registration skipped: components.discovery.port not found " + "(status=%d, port=%d)\n", portStatus, discoveryPort); + return NULL; + } + + /* ---- external hostname (used both as discovery host and as this + * service's advertised hostname) ---- */ + char *externalDomain = NULL; + int hostStatus = cfgGetStringC(configmgr, ZSS_CFGNAME, &externalDomain, + 3, "zowe", "externalDomains", 0); + if (hostStatus != ZCFG_SUCCESS || externalDomain == NULL) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_SEVERE, + ZSS_LOG_EUREKA_NO_DOMAIN_MSG, hostStatus); + return NULL; + } + + /* ---- IPv6: prepare bracketed form for URLs and underscore form for instanceId ---- */ + bool ipv6 = isIPv6Address(externalDomain); + + /* hostForUrl: "[addr]" for IPv6, plain addr otherwise */ + char *hostForUrl; + if (ipv6) { + int bracketLen = strlen(externalDomain) + 3; /* '[' + addr + ']' + NUL */ + hostForUrl = (char *)safeMalloc(bracketLen, "eurekaHostForUrl"); + snprintf(hostForUrl, bracketLen, "[%s]", externalDomain); + } else { + hostForUrl = externalDomain; /* no transformation needed for IPv4 or hostname */ + } + + /* hostForInstanceId: colons become underscores for IPv6 */ + char *hostForInstanceId = ipv6 + ? ipv6ColonsToUnderscores(externalDomain) + : externalDomain; + + /* ---- allocate and populate settings ---- */ + EurekaClientSettings *eurekaSettings = + (EurekaClientSettings *)safeMalloc(sizeof(EurekaClientSettings), + "EurekaClientSettings"); + if (!eurekaSettings) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_SEVERE, + ZSS_LOG_EUREKA_ALLOC_SETTINGS_MSG); + return NULL; + } + memset(eurekaSettings, 0, sizeof(EurekaClientSettings)); + + /* discovery coordinates */ + eurekaSettings->discoveryHost = externalDomain; + eurekaSettings->discoveryPort = discoveryPort; + + /* service identity */ + eurekaSettings->serviceId = "ZSS"; + eurekaSettings->hostName = externalDomain; + eurekaSettings->ipAddr = zssAddress; + eurekaSettings->port = zssPort; + eurekaSettings->securePortEnabled = zssSecure; + + /* instanceId: :: (IPv6 colons -> underscores) */ + { + int idLen = strlen(hostForInstanceId) + 1 /* : */ + 3 /* ZSS */ + 1 + 10 + 1; + char *iid = (char *)safeMalloc(idLen, "EurekaInstanceId"); + snprintf(iid, idLen, "%s:ZSS:%d", hostForInstanceId, zssPort); + eurekaSettings->instanceId = iid; + } + + /* well-known URLs (IPv6 host already bracketed in hostForUrl) */ + { + const char *scheme = zssSecure ? "https" : "http"; + int urlBase = strlen(scheme) + 3 /* :// */ + strlen(hostForUrl) + 1 + 10 + 32; + + char *homeUrl = (char *)safeMalloc(urlBase, "EurekaHomeUrl"); + char *statusUrl = (char *)safeMalloc(urlBase, "EurekaStatusUrl"); + char *healthUrl = (char *)safeMalloc(urlBase, "EurekaHealthUrl"); + + snprintf(homeUrl, urlBase, "%s://%s:%d/", scheme, hostForUrl, zssPort); + snprintf(statusUrl, urlBase, "%s://%s:%d/info", scheme, hostForUrl, zssPort); + snprintf(healthUrl, urlBase, "%s://%s:%d/health", scheme, hostForUrl, zssPort); + + eurekaSettings->homePageUrl = homeUrl; + eurekaSettings->statusPageUrl = statusUrl; + eurekaSettings->healthCheckUrl = healthUrl; + } + + eurekaSettings->version = zssVersion; + + /* timing: read heartbeatIntervalSeconds from YAML, fall back to compile-time default */ + int heartbeatIntervalSeconds = EUREKA_HEARTBEAT_INTERVAL_SECS; + cfgGetIntC(configmgr, ZSS_CFGNAME, &heartbeatIntervalSeconds, + 5, "components", "zss", "agent", "mediationLayer", "heartbeatIntervalSeconds"); + if (heartbeatIntervalSeconds <= 0) { + heartbeatIntervalSeconds = EUREKA_HEARTBEAT_INTERVAL_SECS; + } + + eurekaSettings->heartbeatIntervalSeconds = heartbeatIntervalSeconds; + + /* timing: read retryIntervalSeconds from YAML, fall back to compile-time default */ + int retryIntervalSeconds = EUREKA_RETRY_INTERVAL_SECS; + cfgGetIntC(configmgr, ZSS_CFGNAME, &retryIntervalSeconds, + 5, "components", "zss", "agent", "mediationLayer", "retryIntervalSeconds"); + if (retryIntervalSeconds <= 0) { + retryIntervalSeconds = EUREKA_RETRY_INTERVAL_SECS; + } + + eurekaSettings->retryIntervalSeconds = retryIntervalSeconds; + eurekaSettings->maxRetries = 0; /* retry indefinitely */ + +#ifdef USE_ZOWE_TLS + eurekaSettings->tlsEnv = tlsEnv; +#endif + + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_INFO, + ZSS_LOG_EUREKA_SETTINGS_MSG, + eurekaSettings->discoveryHost, eurekaSettings->discoveryPort, eurekaSettings->instanceId); + + return eurekaSettings; +} + + +/* + * startEurekaClient + * + * Spawns the background RLETask. The task pointer (EurekaClientContext) is + * attached via task->userPointer and is owned by the task. + */ +void startEurekaClient(HttpServer *server, EurekaClientSettings *settings) { + if (!settings) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "startEurekaClient: settings is NULL, skipping\n"); + return; + } + + EurekaClientContext *ctx = + (EurekaClientContext *)safeMalloc(sizeof(EurekaClientContext), + "EurekaClientContext"); + if (!ctx) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_SEVERE, + ZSS_LOG_EUREKA_ALLOC_CTX_MSG); + return; + } + ctx->settings = settings; + + RLETask *task = makeRLETask(server->base->rleAnchor, + RLE_TASK_TCB_CAPABLE | RLE_TASK_DISPOSABLE, + eurekaClientTaskMain); + if (!task) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_SEVERE, + ZSS_LOG_EUREKA_CREATE_TASK_MSG); + safeFree((char *)ctx, sizeof(EurekaClientContext)); + return; + } + + task->userPointer = ctx; + startRLETask(task, NULL); + + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_INFO, + ZSS_LOG_EUREKA_TASK_STARTED_MSG, + settings->discoveryHost, settings->discoveryPort, + settings->instanceId); +} + + +/* ========================================================================= + * RLETask entry point + * ========================================================================= */ + +static int eurekaClientTaskMain(RLETask *task) { + EurekaClientContext *ctx = (EurekaClientContext *)task->userPointer; + EurekaClientSettings *settings = ctx->settings; + + const int heartbeatSecs = (settings->heartbeatIntervalSeconds > 0) + ? settings->heartbeatIntervalSeconds + : EUREKA_HEARTBEAT_INTERVAL_SECS; + const int retrySecs = (settings->retryIntervalSeconds > 0) + ? settings->retryIntervalSeconds + : EUREKA_RETRY_INTERVAL_SECS; + const int warnEvery = 10; /* log a warning every N successive failures */ + + bool registered = false; + int httpStatus = 0; + int rc = EUREKA_RC_OK; + int failCount = 0; + + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_INFO, + ZSS_LOG_EUREKA_REG_LOOP_MSG); + + /* ----------------------------------------------------------------------- + * Outer loop: keep trying to (re)register and heartbeat forever. + * --------------------------------------------------------------------- */ + while (true) { + + /* ---- registration phase ---- */ + if (!registered) { + rc = doRegistration(settings, &httpStatus); + if (rc == EUREKA_RC_OK && (httpStatus == 204 || httpStatus == 200)) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_INFO, + ZSS_LOG_EUREKA_REGISTERED_MSG, httpStatus); + registered = true; + failCount = 0; + } else { + failCount++; + if (failCount == 1 || failCount % warnEvery == 0) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_WARNING, + ZSS_LOG_EUREKA_REG_FAILED_MSG, + rc, httpStatus, retrySecs, failCount); + } + sleep(retrySecs); + continue; + } + } + + /* ---- heartbeat phase ---- */ + sleep(heartbeatSecs); + + rc = doHeartbeat(settings, &httpStatus); + + if (rc == EUREKA_RC_OK && httpStatus == 200) { + /* heartbeat accepted -- all is well */ + failCount = 0; + } else if (rc == EUREKA_RC_NOT_FOUND || httpStatus == 404) { + /* Instance was evicted; force re-registration on next iteration */ + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_WARNING, + ZSS_LOG_EUREKA_HB_404_MSG); + registered = false; + } else { + failCount++; + if (failCount == 1 || failCount % warnEvery == 0) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_WARNING, + ZSS_LOG_EUREKA_HB_FAILED_MSG, + rc, httpStatus, failCount); + } + } + } + + /* unreachable -- task runs for the lifetime of the process */ + return 0; +} + + +/* ========================================================================= + * Registration / heartbeat helpers + * ========================================================================= */ + +/* + * doRegistration -- POST /eureka/apps/ + */ +static int doRegistration(EurekaClientSettings *settings, int *httpStatusOut) { + char urlPath[256]; + if (buildBodyPath(settings, urlPath, sizeof(urlPath)) != 0) { + return EUREKA_RC_STAGE_ERROR; + } + + char body[EUREKA_BODY_BUFSIZE]; + int bodyLen = buildRegistrationBody(settings, body, sizeof(body)); + if (bodyLen <= 0) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_WARNING, + ZSS_LOG_EUREKA_BODY_OVERFLOW_MSG); + return EUREKA_RC_STAGE_ERROR; + } + + return sendEurekaRequest(settings, "POST", urlPath, body, bodyLen, + httpStatusOut); +} + +/* + * doHeartbeat -- PUT /eureka/apps// + */ +static int doHeartbeat(EurekaClientSettings *settings, int *httpStatusOut) { + char urlPath[512]; + if (buildInstancePath(settings, urlPath, sizeof(urlPath)) != 0) { + return EUREKA_RC_STAGE_ERROR; + } + + return sendEurekaRequest(settings, "PUT", urlPath, NULL, 0, httpStatusOut); +} + + +/* ========================================================================= + * Low-level HTTP helper + * ========================================================================= */ + +/* + * sendEurekaRequest + * + * Opens a new HTTP(S) client session, stages and sends a single request, + * reads the response, and returns. Each call creates and destroys its own + * HttpClientContext + HttpClientSession so that the task never holds an + * open socket between heartbeats. + * + * body == NULL means no request body (used for heartbeats). + */ +static int sendEurekaRequest(EurekaClientSettings *settings, + const char *method, + const char *urlPath, + const char *body, + int bodyLen, + int *httpStatusOut) { + int rc = EUREKA_RC_OK; + *httpStatusOut = 0; + + HttpClientSettings clientSettings; + memset(&clientSettings, 0, sizeof(clientSettings)); + clientSettings.host = settings->discoveryHost; + clientSettings.port = settings->discoveryPort; + clientSettings.recvTimeoutSeconds = EUREKA_RECV_TIMEOUT_SECS; + + LoggingContext *logCtx = makeLoggingContext(); + HttpClientContext *httpCtx = NULL; + HttpClientSession *session = NULL; + + do { + /* initialise HTTP context (with or without TLS) */ + int initRc = 0; +#ifdef USE_ZOWE_TLS + if (settings->tlsEnv) { + initRc = httpClientContextInitSecure(&clientSettings, logCtx, + settings->tlsEnv, &httpCtx); + } else { + initRc = httpClientContextInit(&clientSettings, logCtx, &httpCtx); + } +#else + initRc = httpClientContextInit(&clientSettings, logCtx, &httpCtx); +#endif + if (initRc != 0) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "Eureka %s %s: httpClientContextInit rc=%d\n", + method, urlPath, initRc); + rc = EUREKA_RC_CTX_ERROR; + break; + } + + /* open TCP (or TLS) connection */ + int sessionRc = httpClientSessionInit(httpCtx, &session); + if (sessionRc != 0) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "Eureka %s %s: httpClientSessionInit rc=%d\n", + method, urlPath, sessionRc); + rc = EUREKA_RC_SESSION_ERROR; + break; + } + + /* stage the request line and Host header */ + int stageRc = httpClientSessionStageRequest(httpCtx, session, + (char *)method, + (char *)urlPath, + NULL, NULL, /* no basic auth */ + NULL, 0); /* no inline body */ + if (stageRc != 0) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "Eureka %s %s: stageRequest rc=%d\n", method, urlPath, stageRc); + rc = EUREKA_RC_STAGE_ERROR; + break; + } + + /* add required HTTP headers */ + requestStringHeader(session->request, TRUE, "Content-Type", + "application/json"); + requestStringHeader(session->request, TRUE, "Accept", + "application/json"); + + /* attach request body when provided (POST only) */ + if (body != NULL && bodyLen > 0) { + int bodyRc = httpClientSessionSetRequestBody(httpCtx, session, + (char *)body, bodyLen, + TRUE /* bodyIsNativeText */); + if (bodyRc != 0) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "Eureka %s %s: setRequestBody rc=%d\n", + method, urlPath, bodyRc); + rc = EUREKA_RC_STAGE_ERROR; + break; + } + } + + /* send the request */ + int sendRc = httpClientSessionSend(httpCtx, session); + if (sendRc != 0) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "Eureka %s %s: send rc=%d\n", method, urlPath, sendRc); + rc = EUREKA_RC_SEND_ERROR; + break; + } + + /* receive and parse the response */ + int recvRc = httpClientSessionReceiveNativeLoop(httpCtx, session); + if (recvRc != 0) { + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "Eureka %s %s: receive rc=%d\n", method, urlPath, recvRc); + rc = EUREKA_RC_RECV_ERROR; + break; + } + + if (session->response == NULL) { + rc = EUREKA_RC_RECV_ERROR; + break; + } + + *httpStatusOut = session->response->statusCode; + + /* 404 is a special signal to the caller to re-register */ + if (*httpStatusOut == 404) { + rc = EUREKA_RC_NOT_FOUND; + } + + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "Eureka %s %s: HTTP %d\n", method, urlPath, *httpStatusOut); + + } while (0); + + /* clean up */ + if (session) { + httpClientSessionDestroy(session); + } + if (httpCtx) { + httpClientContextDestroy(httpCtx); + } + + return rc; +} + + +/* ========================================================================= + * URL path builders + * ========================================================================= */ + +/* + * buildBodyPath -- /eureka/apps/ + * Used for the initial POST registration. + */ +static int buildBodyPath(EurekaClientSettings *settings, + char *buf, int bufLen) { + int n = snprintf(buf, bufLen, "%s%s", + EUREKA_APPS_PATH_PREFIX, settings->serviceId); + return (n > 0 && n < bufLen) ? 0 : -1; +} + +/* + * buildInstancePath -- /eureka/apps// + * Used for PUT heartbeats. + */ +static int buildInstancePath(EurekaClientSettings *settings, + char *buf, int bufLen) { + int n = snprintf(buf, bufLen, "%s%s/%s", + EUREKA_APPS_PATH_PREFIX, + settings->serviceId, + settings->instanceId); + return (n > 0 && n < bufLen) ? 0 : -1; +} + + +/* ========================================================================= + * JSON body builder + * ========================================================================= */ + +/* + * buildRegistrationBody + * + * Produces the Eureka instance registration JSON as a native (EBCDIC on + * z/OS) null-terminated string. Returns the number of bytes written + * (excluding the NUL), or -1 if the buffer was too small. + * + * Example output (pretty-printed for readability): + * + * { + * "instance": { + * "instanceId": "myhost:ZSS:7557", + * "app": "ZSS", + * "hostName": "myhost", + * "ipAddr": "1.2.3.4", + * "vipAddress": "zss", + * "secureVipAddress":"zss", + * "status": "UP", + * "port": { "$": 7557, "@enabled": "false" }, + * "securePort": { "$": 7557, "@enabled": "true" }, + * "homePageUrl": "https://myhost:7557/", + * "statusPageUrl": "https://myhost:7557/info", + * "healthCheckUrl": "https://myhost:7557/health", + * "dataCenterInfo": { + * "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", + * "name": "MyOwn" + * }, + * "leaseInfo": { + * "renewalIntervalInSecs": 30, + * "durationInSecs": 90 + * }, + * "metadata": { + * "apiml.service.title": "Zowe ZSS", + * "apiml.service.description": "Zowe ZSS Service (z/OS Systems Services)" + * } + * } + * } + */ +static int buildRegistrationBody(EurekaClientSettings *settings, + char *buf, int bufLen) { + const char *portEnabled = settings->securePortEnabled ? "false" : "true"; + const char *securePortEnabled = settings->securePortEnabled ? "true" : "false"; + int renewalSecs = (settings->heartbeatIntervalSeconds > 0) + ? settings->heartbeatIntervalSeconds + : EUREKA_HEARTBEAT_INTERVAL_SECS; + int durationSecs = renewalSecs * 3; /* Eureka convention: 3x renewal */ + + int n = snprintf(buf, bufLen, + "{" + "\"instance\":{" + "\"instanceId\":\"%s\"," + "\"app\":\"%s\"," + "\"hostName\":\"%s\"," + "\"ipAddr\":\"%s\"," + "\"vipAddress\":\"zss\"," + "\"secureVipAddress\":\"zss\"," + "\"status\":\"UP\"," + "\"port\":{\"$\":%d,\"@enabled\":\"%s\"}," + "\"securePort\":{\"$\":%d,\"@enabled\":\"%s\"}," + "\"homePageUrl\":\"%s\"," + "\"statusPageUrl\":\"%s\"," + "\"healthCheckUrl\":\"%s\"," + "\"dataCenterInfo\":{" + "\"@class\":\"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo\"," + "\"name\":\"MyOwn\"" + "}," + "\"leaseInfo\":{" + "\"renewalIntervalInSecs\":%d," + "\"durationInSecs\":%d" + "}," + "\"metadata\":{" + "\"apiml.service.title\":\"Zowe ZSS\"," + "\"apiml.service.description\":\"Zowe ZSS Service (z/OS Systems Services)\"," + "\"apiml.service.version\":\"%s\"" + "}" + "}" + "}", + settings->instanceId, + settings->serviceId, + settings->hostName, + settings->ipAddr, + settings->port, portEnabled, + settings->port, securePortEnabled, + settings->homePageUrl, + settings->statusPageUrl, + settings->healthCheckUrl, + renewalSecs, + durationSecs, + settings->version ? settings->version : "" + ); + + if (n <= 0 || n >= bufLen) { + return -1; + } + return n; +} + + +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ diff --git a/c/zss.c b/c/zss.c index c4b855f5d..2e1642c46 100644 --- a/c/zss.c +++ b/c/zss.c @@ -86,6 +86,7 @@ #include "storageApiml.h" #include "passTicketService.h" #include "jwk.h" +#include "eurekaClient.h" #include "zss.h" #define PRODUCT "ZLUX" @@ -1073,6 +1074,7 @@ static void initLoggingComponents(void) { logConfigureComponent(NULL, LOG_COMP_ID_CTDS, "CT/DS", LOG_DEST_PRINTF_STDOUT, ZOWE_LOG_INFO); logConfigureComponent(NULL, LOG_COMP_ID_APIML_STORAGE, "APIML Storage", LOG_DEST_PRINTF_STDOUT, ZOWE_LOG_INFO); logConfigureComponent(NULL, LOG_COMP_ID_JWK, "JWK", LOG_DEST_PRINTF_STDOUT, ZOWE_LOG_INFO); + logConfigureComponent(NULL, LOG_COMP_ID_EUREKA, "Eureka Client", LOG_DEST_PRINTF_STDOUT, ZOWE_LOG_INFO); zowelog(NULL, LOG_COMP_ID_MVD_SERVER, ZOWE_LOG_INFO, ZSS_LOG_ZSS_START_VER_MSG, productVersion); } @@ -1872,6 +1874,14 @@ int main(int argc, char **argv){ loadWebServerConfigV2(server, configmgr, htUsers, htGroups, defaultSeconds); readWebPluginDefinitions(server, slh, pluginsDir, configmgr, apimlStorageSettings); configureJwt(server, jwkSettings); + EurekaClientSettings *eurekaSettings = + makeEurekaClientSettings(slh, configmgr, address, port, isHttpsConfigured, + productVersion +#ifdef USE_ZOWE_TLS + , tlsEnv +#endif + ); + startEurekaClient(server, eurekaSettings); installUserMappingService(server); installUnixFileContentsService(server); installUnixFileRenameService(server); diff --git a/defaults.yaml b/defaults.yaml index 83dc4a752..4b85db550 100644 --- a/defaults.yaml +++ b/defaults.yaml @@ -37,4 +37,7 @@ components: enabled: ${{ components['caching-service']?.enabled || components.apiml?.enabled }} enabled: ${{ components.discovery?.enabled || components.apiml?.enabled }} serviceName: "zss" + static: false + heartbeatIntervalSeconds: 30 + retryIntervalSeconds: 10 handshakeTimeout: 30000 diff --git a/h/eurekaClient.h b/h/eurekaClient.h new file mode 100644 index 000000000..6472ba54c --- /dev/null +++ b/h/eurekaClient.h @@ -0,0 +1,136 @@ +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ + +/* + * eurekaClient.h + * + * Registers ZSS as a service with the Zowe API ML Discovery server (Eureka) + * and maintains the registration via periodic heartbeats. All network I/O + * runs inside a dedicated RLETask so the main HTTP server loop is not blocked. + * + * Typical call sequence from zss.c: + * + * EurekaClientSettings *eurekaSettings = + * makeEurekaClientSettings(slh, configmgr, address, port, isHttps, tlsEnv); + * startEurekaClient(server, eurekaSettings); + */ + +#ifndef ZSS_EUREKACLIENT_H_ +#define ZSS_EUREKACLIENT_H_ + +#ifdef METTLE +#error Metal C is not supported by eurekaClient +#endif + +#include +#include "zowetypes.h" +#include "alloc.h" +#include "httpserver.h" +#include "configmgr.h" +#ifdef USE_ZOWE_TLS +#include "tls.h" +#endif + +/* --------------------------------------------------------------------------- + * EurekaClientSettings + * + * All char* fields are expected to remain valid for the lifetime of the + * background task; the recommended approach is to allocate them from the + * server's ShortLivedHeap (slh) or from safeMalloc. + * --------------------------------------------------------------------------- */ +typedef struct EurekaClientSettings_tag { + + /* ---- Discovery server (API ML Discovery) coordinates ---- */ + char *discoveryHost; /* hostname / IP of the discovery service */ + int discoveryPort; /* TCP port of the discovery service */ + + /* ---- This service's Eureka identity ---- */ + char *serviceId; /* Eureka appId (upper-case), e.g. "ZSS" */ + char *instanceId; /* :: */ + char *hostName; /* hostname this service is reachable at */ + char *ipAddr; /* IP address this service is reachable at */ + int port; /* TCP port this service listens on */ + bool securePortEnabled; /* true when TLS is in use for this service */ + + /* ---- Well-known URL suffixes for Eureka metadata ---- */ + char *homePageUrl; /* e.g. "https://:/" */ + char *statusPageUrl; /* e.g. "https://:/info" */ + char *healthCheckUrl; /* e.g. "https://:/health" */ + + /* ---- Service version reported in Eureka metadata ---- */ + char *version; /* e.g. "2.17.0+20260101" */ + + /* ---- Timing ---- */ + int heartbeatIntervalSeconds; /* PUT heartbeat interval (default: 30 s) */ + int retryIntervalSeconds; /* retry after failed req (default: 30 s) */ + int maxRetries; /* 0 means retry indefinitely */ + +#ifdef USE_ZOWE_TLS + /* TLS environment used to connect to the discovery server */ + TlsEnvironment *tlsEnv; +#endif + +} EurekaClientSettings; + +/* --------------------------------------------------------------------------- + * makeEurekaClientSettings + * + * Reads the Zowe YAML via configmgr to determine the discovery server + * host/port, then constructs an EurekaClientSettings that describes this + * ZSS instance. + * + * Parameters: + * slh - short-lived heap used for string allocations + * configmgr - the live ConfigManager instance + * zssAddress - the IP/hostname ZSS is bound to (from agent settings) + * zssPort - the TCP port ZSS is listening on + * zssSecure - true when ZSS is serving HTTPS + * zssVersion - product version string (e.g. productVersion from zss.c) + * tlsEnv - TLS environment for outbound connections (may be NULL) + * + * Returns NULL when the discovery component is not enabled or required + * configuration values are absent. + * --------------------------------------------------------------------------- */ +EurekaClientSettings *makeEurekaClientSettings(ShortLivedHeap *slh, + ConfigManager *configmgr, + const char *zssAddress, + int zssPort, + bool zssSecure, + const char *zssVersion +#ifdef USE_ZOWE_TLS + , TlsEnvironment *tlsEnv +#endif + ); + +/* --------------------------------------------------------------------------- + * startEurekaClient + * + * Creates a disposable RLETask that: + * 1. Posts a registration request to /eureka/apps/. + * 2. Sends periodic PUT heartbeats to keep the registration alive. + * + * The task retries on transient network errors according to + * settings->retryIntervalSecs. It runs until the process exits. + * + * Safe to call with settings == NULL (no-op). + * --------------------------------------------------------------------------- */ +void startEurekaClient(HttpServer *server, EurekaClientSettings *settings); + +#endif /* ZSS_EUREKACLIENT_H_ */ + +/* + This program and the accompanying materials are + made available under the terms of the Eclipse Public License v2.0 which accompanies + this distribution, and is available at https://www.eclipse.org/legal/epl-v20.html + + SPDX-License-Identifier: EPL-2.0 + + Copyright Contributors to the Zowe Project. +*/ diff --git a/h/zssLogging.h b/h/zssLogging.h index 74a0f400f..14b644341 100644 --- a/h/zssLogging.h +++ b/h/zssLogging.h @@ -42,6 +42,7 @@ #define LOG_COMP_ID_DATASERVICE 0x008F000300050000 #define LOG_COMP_ID_APIML_STORAGE 0x008F000300060000 #define LOG_COMP_ID_JWK 0x008F000300070000 +#define LOG_COMP_ID_EUREKA 0x008F000300080000 #define ZSS_LOGGING_COMPONENTS_MAP(zssLogComponents)\ static const LogComponentsMap zssLogComponents[] = {\ @@ -52,6 +53,7 @@ {LOG_COMP_ID_DATASERVICE, "dataservice"},\ {LOG_COMP_ID_APIML_STORAGE, "apimlstorage"},\ {LOG_COMP_ID_JWK, "jwk"},\ + {LOG_COMP_ID_EUREKA, "eureka"},\ {0, NULL}\ }; @@ -527,6 +529,80 @@ bool isLogLevelValid(int level); #define ZSS_LOG_JWK_RETRY_MSG_TEXT "Failed to get JWK. rc=%s (%d), rsn=%s (%d). Retry in %d seconds\n" #define ZSS_LOG_JWK_RETRY_MSG ZSS_LOG_JWK_RETRY_MSG_ID" "ZSS_LOG_JWK_RETRY_MSG_TEXT +/* Eureka client */ + +#ifndef ZSS_LOG_EUREKA_SETTINGS_MSG_ID +#define ZSS_LOG_EUREKA_SETTINGS_MSG_ID ZSS_LOG_MSG_PRFX"1700I" +#endif +#define ZSS_LOG_EUREKA_SETTINGS_MSG_TEXT "Eureka settings: discovery=%s:%d instanceId=%s\n" +#define ZSS_LOG_EUREKA_SETTINGS_MSG ZSS_LOG_EUREKA_SETTINGS_MSG_ID" "ZSS_LOG_EUREKA_SETTINGS_MSG_TEXT + +#ifndef ZSS_LOG_EUREKA_TASK_STARTED_MSG_ID +#define ZSS_LOG_EUREKA_TASK_STARTED_MSG_ID ZSS_LOG_MSG_PRFX"1701I" +#endif +#define ZSS_LOG_EUREKA_TASK_STARTED_MSG_TEXT "Eureka client task started (discovery=%s:%d, instanceId=%s)\n" +#define ZSS_LOG_EUREKA_TASK_STARTED_MSG ZSS_LOG_EUREKA_TASK_STARTED_MSG_ID" "ZSS_LOG_EUREKA_TASK_STARTED_MSG_TEXT + +#ifndef ZSS_LOG_EUREKA_REG_LOOP_MSG_ID +#define ZSS_LOG_EUREKA_REG_LOOP_MSG_ID ZSS_LOG_MSG_PRFX"1702I" +#endif +#define ZSS_LOG_EUREKA_REG_LOOP_MSG_TEXT "Eureka client task: beginning registration loop\n" +#define ZSS_LOG_EUREKA_REG_LOOP_MSG ZSS_LOG_EUREKA_REG_LOOP_MSG_ID" "ZSS_LOG_EUREKA_REG_LOOP_MSG_TEXT + +#ifndef ZSS_LOG_EUREKA_REGISTERED_MSG_ID +#define ZSS_LOG_EUREKA_REGISTERED_MSG_ID ZSS_LOG_MSG_PRFX"1703I" +#endif +#define ZSS_LOG_EUREKA_REGISTERED_MSG_TEXT "Eureka: registered successfully (HTTP %d)\n" +#define ZSS_LOG_EUREKA_REGISTERED_MSG ZSS_LOG_EUREKA_REGISTERED_MSG_ID" "ZSS_LOG_EUREKA_REGISTERED_MSG_TEXT + +#ifndef ZSS_LOG_EUREKA_ALLOC_SETTINGS_MSG_ID +#define ZSS_LOG_EUREKA_ALLOC_SETTINGS_MSG_ID ZSS_LOG_MSG_PRFX"1704E" +#endif +#define ZSS_LOG_EUREKA_ALLOC_SETTINGS_MSG_TEXT "Eureka: failed to allocate settings\n" +#define ZSS_LOG_EUREKA_ALLOC_SETTINGS_MSG ZSS_LOG_EUREKA_ALLOC_SETTINGS_MSG_ID" "ZSS_LOG_EUREKA_ALLOC_SETTINGS_MSG_TEXT + +#ifndef ZSS_LOG_EUREKA_ALLOC_CTX_MSG_ID +#define ZSS_LOG_EUREKA_ALLOC_CTX_MSG_ID ZSS_LOG_MSG_PRFX"1705E" +#endif +#define ZSS_LOG_EUREKA_ALLOC_CTX_MSG_TEXT "Eureka: failed to allocate client context\n" +#define ZSS_LOG_EUREKA_ALLOC_CTX_MSG ZSS_LOG_EUREKA_ALLOC_CTX_MSG_ID" "ZSS_LOG_EUREKA_ALLOC_CTX_MSG_TEXT + +#ifndef ZSS_LOG_EUREKA_CREATE_TASK_MSG_ID +#define ZSS_LOG_EUREKA_CREATE_TASK_MSG_ID ZSS_LOG_MSG_PRFX"1706E" +#endif +#define ZSS_LOG_EUREKA_CREATE_TASK_MSG_TEXT "Eureka: failed to create background RLETask\n" +#define ZSS_LOG_EUREKA_CREATE_TASK_MSG ZSS_LOG_EUREKA_CREATE_TASK_MSG_ID" "ZSS_LOG_EUREKA_CREATE_TASK_MSG_TEXT + +#ifndef ZSS_LOG_EUREKA_REG_FAILED_MSG_ID +#define ZSS_LOG_EUREKA_REG_FAILED_MSG_ID ZSS_LOG_MSG_PRFX"1707W" +#endif +#define ZSS_LOG_EUREKA_REG_FAILED_MSG_TEXT "Eureka: registration failed (rc=%d, HTTP %d), retry in %d s (attempt %d)\n" +#define ZSS_LOG_EUREKA_REG_FAILED_MSG ZSS_LOG_EUREKA_REG_FAILED_MSG_ID" "ZSS_LOG_EUREKA_REG_FAILED_MSG_TEXT + +#ifndef ZSS_LOG_EUREKA_HB_404_MSG_ID +#define ZSS_LOG_EUREKA_HB_404_MSG_ID ZSS_LOG_MSG_PRFX"1708W" +#endif +#define ZSS_LOG_EUREKA_HB_404_MSG_TEXT "Eureka: heartbeat returned 404; will re-register\n" +#define ZSS_LOG_EUREKA_HB_404_MSG ZSS_LOG_EUREKA_HB_404_MSG_ID" "ZSS_LOG_EUREKA_HB_404_MSG_TEXT + +#ifndef ZSS_LOG_EUREKA_HB_FAILED_MSG_ID +#define ZSS_LOG_EUREKA_HB_FAILED_MSG_ID ZSS_LOG_MSG_PRFX"1709W" +#endif +#define ZSS_LOG_EUREKA_HB_FAILED_MSG_TEXT "Eureka: heartbeat failed (rc=%d, HTTP %d), attempt %d\n" +#define ZSS_LOG_EUREKA_HB_FAILED_MSG ZSS_LOG_EUREKA_HB_FAILED_MSG_ID" "ZSS_LOG_EUREKA_HB_FAILED_MSG_TEXT + +#ifndef ZSS_LOG_EUREKA_BODY_OVERFLOW_MSG_ID +#define ZSS_LOG_EUREKA_BODY_OVERFLOW_MSG_ID ZSS_LOG_MSG_PRFX"1710W" +#endif +#define ZSS_LOG_EUREKA_BODY_OVERFLOW_MSG_TEXT "Eureka: registration body buffer too small\n" +#define ZSS_LOG_EUREKA_BODY_OVERFLOW_MSG ZSS_LOG_EUREKA_BODY_OVERFLOW_MSG_ID" "ZSS_LOG_EUREKA_BODY_OVERFLOW_MSG_TEXT + +#ifndef ZSS_LOG_EUREKA_NO_DOMAIN_MSG_ID +#define ZSS_LOG_EUREKA_NO_DOMAIN_MSG_ID ZSS_LOG_MSG_PRFX"1711E" +#endif +#define ZSS_LOG_EUREKA_NO_DOMAIN_MSG_TEXT "Eureka registration skipped: zowe.externalDomains[0] not found (status=%d)\n" +#define ZSS_LOG_EUREKA_NO_DOMAIN_MSG ZSS_LOG_EUREKA_NO_DOMAIN_MSG_ID" "ZSS_LOG_EUREKA_NO_DOMAIN_MSG_TEXT + #endif /* MVD_H_ZSSLOGGING_H_ */ diff --git a/manifest.template.yaml b/manifest.template.yaml index 1227158e0..cd836da4d 100644 --- a/manifest.template.yaml +++ b/manifest.template.yaml @@ -30,8 +30,4 @@ commands: install: bin/internal-install.sh start: bin/start.sh configure: bin/configure.sh - validate: bin/validate.sh -# we do not specify encoding here because its already tagged ascii -apimlServices: - static: - - file: apiml-static-reg.yaml.template + validate: bin/validate.sh \ No newline at end of file diff --git a/schemas/zss-config.json b/schemas/zss-config.json index 70ee28155..0295ab5f3 100644 --- a/schemas/zss-config.json +++ b/schemas/zss-config.json @@ -86,6 +86,21 @@ "description": "Controls whether the app-server storage API can store in the caching service" } } + }, + "heartbeatIntervalSeconds": { + "type": "number", + "default": 30, + "description": "Interval in seconds between Eureka heartbeat renewal requests sent to the API Mediation Layer Discovery service" + }, + "retryIntervalSeconds": { + "type": "number", + "default": 10, + "description": "Interval in seconds to wait before retrying a failed Eureka registration or heartbeat request" + }, + "static": { + "type": "boolean", + "default": false, + "description": "When true, ZSS uses APIML static registration instead of APIML eureka registration." } } }, @@ -384,6 +399,10 @@ "description": "Controls logging of JWT library functions.", "$ref": "#/$defs/logLevel" }, + "_zss.eureka": { + "description": "Controls logging of APIML eureka registration.", + "$ref": "#/$defs/logLevel" + }, "^.*$": { "$ref": "#/$defs/logLevel" } From 53a723b6b0c608fc11ba80d522ca234de2bec8bc Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Mon, 9 Mar 2026 16:27:20 -0400 Subject: [PATCH 2/9] Update eurekaClient.h Signed-off-by: 1000TurquoisePogs --- h/eurekaClient.h | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/h/eurekaClient.h b/h/eurekaClient.h index 6472ba54c..2b09fe6ed 100644 --- a/h/eurekaClient.h +++ b/h/eurekaClient.h @@ -48,29 +48,29 @@ typedef struct EurekaClientSettings_tag { /* ---- Discovery server (API ML Discovery) coordinates ---- */ - char *discoveryHost; /* hostname / IP of the discovery service */ - int discoveryPort; /* TCP port of the discovery service */ + char *discoveryHost; /* hostname / IP of the discovery service */ + int discoveryPort; /* TCP port of the discovery service */ /* ---- This service's Eureka identity ---- */ - char *serviceId; /* Eureka appId (upper-case), e.g. "ZSS" */ - char *instanceId; /* :: */ - char *hostName; /* hostname this service is reachable at */ - char *ipAddr; /* IP address this service is reachable at */ - int port; /* TCP port this service listens on */ - bool securePortEnabled; /* true when TLS is in use for this service */ + char *serviceId; /* Eureka appId (upper-case), e.g. "ZSS" */ + char *instanceId; /* :: */ + char *hostName; /* hostname this service is reachable at */ + const char *ipAddr; /* IP address this service is reachable at */ + int port; /* TCP port this service listens on */ + bool securePortEnabled; /* true when TLS is in use for this service */ /* ---- Well-known URL suffixes for Eureka metadata ---- */ - char *homePageUrl; /* e.g. "https://:/" */ - char *statusPageUrl; /* e.g. "https://:/info" */ - char *healthCheckUrl; /* e.g. "https://:/health" */ + char *homePageUrl; /* e.g. "https://:/" */ + char *statusPageUrl; /* e.g. "https://:/info" */ + char *healthCheckUrl; /* e.g. "https://:/health" */ /* ---- Service version reported in Eureka metadata ---- */ - char *version; /* e.g. "2.17.0+20260101" */ + const char *version; /* e.g. "2.17.0+20260101" */ /* ---- Timing ---- */ - int heartbeatIntervalSeconds; /* PUT heartbeat interval (default: 30 s) */ - int retryIntervalSeconds; /* retry after failed req (default: 30 s) */ - int maxRetries; /* 0 means retry indefinitely */ + int heartbeatIntervalSeconds; /* PUT heartbeat interval (default: 30 s) */ + int retryIntervalSeconds; /* retry after failed req (default: 30 s) */ + int maxRetries; /* 0 means retry indefinitely */ #ifdef USE_ZOWE_TLS /* TLS environment used to connect to the discovery server */ From d9ce1bf2b0794e4f348855d6bbbf9643b31b679a Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Tue, 10 Mar 2026 08:44:12 -0400 Subject: [PATCH 3/9] Point at fix branch of zcc Signed-off-by: 1000TurquoisePogs --- deps/zowe-common-c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/zowe-common-c b/deps/zowe-common-c index 8cf887c8c..37f289699 160000 --- a/deps/zowe-common-c +++ b/deps/zowe-common-c @@ -1 +1 @@ -Subproject commit 8cf887c8c9959e3e6a0bd2544f87159dc98b4c91 +Subproject commit 37f28969948674f574065534300ce869bb152559 From f5224dff777dc9965653a9468b0b2c523a77d1eb Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Tue, 10 Mar 2026 14:19:12 -0400 Subject: [PATCH 4/9] Update build_zss64.sh with new file Signed-off-by: 1000TurquoisePogs --- build/build_zss64.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/build/build_zss64.sh b/build/build_zss64.sh index f07d63221..b7c7558f9 100755 --- a/build/build_zss64.sh +++ b/build/build_zss64.sh @@ -252,6 +252,7 @@ if ! c89 \ ${ZSS}/c/passTicketService.c \ ${ZSS}/c/jwk.c \ ${ZSS}/c/safIdtService.c \ + ${ZSS}/c/eurekaClient.c \ ${GSKLIB} ; then extattr +p ${ZSS}/bin/zssServer64 From 638ee6b1a3f4f34569fb848b5459ddaa03173ef6 Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Tue, 10 Mar 2026 14:46:37 -0400 Subject: [PATCH 5/9] Add debug message and change service id to lowercase Signed-off-by: 1000TurquoisePogs --- c/eurekaClient.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/c/eurekaClient.c b/c/eurekaClient.c index 0c8f9346a..4ebd4c0ed 100644 --- a/c/eurekaClient.c +++ b/c/eurekaClient.c @@ -228,7 +228,7 @@ EurekaClientSettings *makeEurekaClientSettings(ShortLivedHeap *slh, eurekaSettings->discoveryPort = discoveryPort; /* service identity */ - eurekaSettings->serviceId = "ZSS"; + eurekaSettings->serviceId = "zss"; eurekaSettings->hostName = externalDomain; eurekaSettings->ipAddr = zssAddress; eurekaSettings->port = zssPort; @@ -436,6 +436,9 @@ static int doRegistration(EurekaClientSettings *settings, int *httpStatusOut) { return EUREKA_RC_STAGE_ERROR; } + zowelog(NULL, LOG_COMP_ID_EUREKA, ZOWE_LOG_DEBUG, + "Eureka registration body: %s\n", body); + return sendEurekaRequest(settings, "POST", urlPath, body, bodyLen, httpStatusOut); } From 62cec53b21e4fba0f58f0989658aa28af5f98b29 Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Tue, 10 Mar 2026 15:39:13 -0400 Subject: [PATCH 6/9] Update eureka metadata Signed-off-by: 1000TurquoisePogs --- c/eurekaClient.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/c/eurekaClient.c b/c/eurekaClient.c index 4ebd4c0ed..953376f8d 100644 --- a/c/eurekaClient.c +++ b/c/eurekaClient.c @@ -688,7 +688,6 @@ static int buildRegistrationBody(EurekaClientSettings *settings, "\"hostName\":\"%s\"," "\"ipAddr\":\"%s\"," "\"vipAddress\":\"zss\"," - "\"secureVipAddress\":\"zss\"," "\"status\":\"UP\"," "\"port\":{\"$\":%d,\"@enabled\":\"%s\"}," "\"securePort\":{\"$\":%d,\"@enabled\":\"%s\"}," @@ -704,8 +703,24 @@ static int buildRegistrationBody(EurekaClientSettings *settings, "\"durationInSecs\":%d" "}," "\"metadata\":{" - "\"apiml.service.title\":\"Zowe ZSS\"," - "\"apiml.service.description\":\"Zowe ZSS Service (z/OS Systems Services)\"," + "\"apiml.routes.api__v1.gatewayUrl\": \"/api/v1\"," + "\"apiml.routes.api__v1.serviceUrl\": \"\"," + "\"apiml.routes.ui__v1.gatewayUrl\": \"/ui/v1\"," + "\"apiml.routes.ui__v1.serviceUrl\": \"\"," + "\"apiml.routes.ws__v1.gatewayUrl\": \"/ws/v1\"," + "\"apiml.routes.ws__v1.serviceUrl\": \"\"," + "\"apiml.apiInfo.0.apiId\": \"org.zowe.zss\"," + "\"apiml.apiInfo.0.gatewayUrl\": \"api/v1\"," + "\"apiml.apiInfo.0.swaggerUrl\": \"https://rs28.rocketsoftware.com:12326/api-docs/agent\"," + "\"apiml.apiInfo.0.version\": \"1.0.0\"," + "\"apiml.catalog.tile.id\": \"zss\"," + "\"apiml.catalog.tile.title\": \"Zowe System Services (ZSS)\"," + "\"apiml.catalog.tile.description\": \"Zowe System Services is an HTTPS and Websocket server that makes it easy to have secure, powerful web APIs backed by low-level z/OS constructs. It contains services for essential z/OS abilities such as working with files, datasets, and ESMs, but is also extensible by REST and Websocket \\\"Dataservices\\\" which are optionally present in App Framework \\\"Plugins\\\".\"," + "\"apiml.catalog.tile.version\": \"3.5.0\"," + "\"apiml.service.description\": \"This list includes core APIs for management of plugins, management of the server itself, and APIs brought by plugins and the app server agent, ZSS. Plugins that do not bring their own API documentation are shown here as stubs.\"," + "\"apiml.authentication.sso\": \"true\"," + "\"apiml.authentication.scheme\": \"zoweJwt\"," + "\"apiml.service.description\":\"Zowe System Services (ZSS)\"," "\"apiml.service.version\":\"%s\"" "}" "}" From 3a59818668926c2fd06e8f75f35e2ced697f4b79 Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Wed, 11 Mar 2026 08:44:54 -0400 Subject: [PATCH 7/9] Update eureka metadata after testing Signed-off-by: 1000TurquoisePogs --- c/eurekaClient.c | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/c/eurekaClient.c b/c/eurekaClient.c index 953376f8d..e7a9eeebb 100644 --- a/c/eurekaClient.c +++ b/c/eurekaClient.c @@ -238,7 +238,7 @@ EurekaClientSettings *makeEurekaClientSettings(ShortLivedHeap *slh, { int idLen = strlen(hostForInstanceId) + 1 /* : */ + 3 /* ZSS */ + 1 + 10 + 1; char *iid = (char *)safeMalloc(idLen, "EurekaInstanceId"); - snprintf(iid, idLen, "%s:ZSS:%d", hostForInstanceId, zssPort); + snprintf(iid, idLen, "%s:zss:%d", hostForInstanceId, zssPort); eurekaSettings->instanceId = iid; } @@ -644,8 +644,8 @@ static int buildInstancePath(EurekaClientSettings *settings, * * { * "instance": { - * "instanceId": "myhost:ZSS:7557", - * "app": "ZSS", + * "instanceId": "myhost:zss:7557", + * "app": "zss", * "hostName": "myhost", * "ipAddr": "1.2.3.4", * "vipAddress": "zss", @@ -688,6 +688,7 @@ static int buildRegistrationBody(EurekaClientSettings *settings, "\"hostName\":\"%s\"," "\"ipAddr\":\"%s\"," "\"vipAddress\":\"zss\"," + "\"secureVipAddress\":\"zss\"," "\"status\":\"UP\"," "\"port\":{\"$\":%d,\"@enabled\":\"%s\"}," "\"securePort\":{\"$\":%d,\"@enabled\":\"%s\"}," @@ -703,24 +704,22 @@ static int buildRegistrationBody(EurekaClientSettings *settings, "\"durationInSecs\":%d" "}," "\"metadata\":{" - "\"apiml.routes.api__v1.gatewayUrl\": \"/api/v1\"," - "\"apiml.routes.api__v1.serviceUrl\": \"\"," - "\"apiml.routes.ui__v1.gatewayUrl\": \"/ui/v1\"," - "\"apiml.routes.ui__v1.serviceUrl\": \"\"," - "\"apiml.routes.ws__v1.gatewayUrl\": \"/ws/v1\"," - "\"apiml.routes.ws__v1.serviceUrl\": \"\"," + "\"apiml.routes.api_v1.gatewayUrl\": \"/api/v1\"," + "\"apiml.routes.api_v1.serviceUrl\": \"/\"," + "\"apiml.routes.ws_v1.gatewayUrl\": \"/ws/v1\"," + "\"apiml.routes.ws_v1.serviceUrl\": \"/\"," "\"apiml.apiInfo.0.apiId\": \"org.zowe.zss\"," "\"apiml.apiInfo.0.gatewayUrl\": \"api/v1\"," - "\"apiml.apiInfo.0.swaggerUrl\": \"https://rs28.rocketsoftware.com:12326/api-docs/agent\"," - "\"apiml.apiInfo.0.version\": \"1.0.0\"," + "\"apiml.apiInfo.0.swaggerUrl\": \"https://TODO/api-docs/agent\"," + "\"apiml.apiInfo.0.version\": \"%s\"," "\"apiml.catalog.tile.id\": \"zss\"," "\"apiml.catalog.tile.title\": \"Zowe System Services (ZSS)\"," - "\"apiml.catalog.tile.description\": \"Zowe System Services is an HTTPS and Websocket server that makes it easy to have secure, powerful web APIs backed by low-level z/OS constructs. It contains services for essential z/OS abilities such as working with files, datasets, and ESMs, but is also extensible by REST and Websocket \\\"Dataservices\\\" which are optionally present in App Framework \\\"Plugins\\\".\"," - "\"apiml.catalog.tile.version\": \"3.5.0\"," - "\"apiml.service.description\": \"This list includes core APIs for management of plugins, management of the server itself, and APIs brought by plugins and the app server agent, ZSS. Plugins that do not bring their own API documentation are shown here as stubs.\"," + "\"apiml.catalog.tile.description\": \"Zowe System Services is an HTTPS and Websocket server that makes it easy to have secure, powerful web APIs backed by low-level z/OS constructs. It contains services for essential z/OS abilities such as working with files, datasets, and ESMs, but is also extensible by REST and Websocket Dataservices which are optionally present in App Framework Plugins.\"," + "\"apiml.catalog.tile.version\": \"%s\"," "\"apiml.authentication.sso\": \"true\"," "\"apiml.authentication.scheme\": \"zoweJwt\"," "\"apiml.service.description\":\"Zowe System Services (ZSS)\"," + "\"apiml.service.title\":\"Zowe System Services (ZSS)\"," "\"apiml.service.version\":\"%s\"" "}" "}" @@ -736,7 +735,9 @@ static int buildRegistrationBody(EurekaClientSettings *settings, settings->healthCheckUrl, renewalSecs, durationSecs, - settings->version ? settings->version : "" + settings->version ? settings->version : "1", + settings->version ? settings->version : "1", + settings->version ? settings->version : "1" ); if (n <= 0 || n >= bufLen) { From 81a7c917e027f560f9f339c2d5b7741fe788c6ec Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Wed, 11 Mar 2026 08:59:36 -0400 Subject: [PATCH 8/9] Switch eureka to false unless HA Signed-off-by: 1000TurquoisePogs --- c/eurekaClient.c | 28 ++++++++++++++++++++++++---- defaults.yaml | 2 +- h/eurekaClient.h | 1 + 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/c/eurekaClient.c b/c/eurekaClient.c index e7a9eeebb..abee3fdc2 100644 --- a/c/eurekaClient.c +++ b/c/eurekaClient.c @@ -260,6 +260,24 @@ EurekaClientSettings *makeEurekaClientSettings(ShortLivedHeap *slh, eurekaSettings->healthCheckUrl = healthUrl; } + /* swagger URL: uses app-server port from components.app-server.port */ + { + int appServerPort = 0; + cfgGetIntC(configmgr, ZSS_CFGNAME, &appServerPort, + 3, "components", "app-server", "port"); + if (appServerPort > 0) { + const char *scheme = zssSecure ? "https" : "http"; + int swaggerUrlLen = strlen(scheme) + 3 + strlen(hostForUrl) + 1 + 10 + + sizeof("/api-docs/agent"); + char *swaggerUrl = (char *)safeMalloc(swaggerUrlLen, "EurekaSwaggerUrl"); + snprintf(swaggerUrl, swaggerUrlLen, "%s://%s:%d/api-docs/agent", + scheme, hostForUrl, appServerPort); + eurekaSettings->swaggerUrl = swaggerUrl; + } else { + eurekaSettings->swaggerUrl = NULL; + } + } + eurekaSettings->version = zssVersion; /* timing: read heartbeatIntervalSeconds from YAML, fall back to compile-time default */ @@ -675,6 +693,7 @@ static int buildRegistrationBody(EurekaClientSettings *settings, char *buf, int bufLen) { const char *portEnabled = settings->securePortEnabled ? "false" : "true"; const char *securePortEnabled = settings->securePortEnabled ? "true" : "false"; + const char *version = (settings->version && settings->version[0]) ? settings->version : "1"; int renewalSecs = (settings->heartbeatIntervalSeconds > 0) ? settings->heartbeatIntervalSeconds : EUREKA_HEARTBEAT_INTERVAL_SECS; @@ -710,7 +729,7 @@ static int buildRegistrationBody(EurekaClientSettings *settings, "\"apiml.routes.ws_v1.serviceUrl\": \"/\"," "\"apiml.apiInfo.0.apiId\": \"org.zowe.zss\"," "\"apiml.apiInfo.0.gatewayUrl\": \"api/v1\"," - "\"apiml.apiInfo.0.swaggerUrl\": \"https://TODO/api-docs/agent\"," + "\"apiml.apiInfo.0.swaggerUrl\": \"%s\"," "\"apiml.apiInfo.0.version\": \"%s\"," "\"apiml.catalog.tile.id\": \"zss\"," "\"apiml.catalog.tile.title\": \"Zowe System Services (ZSS)\"," @@ -735,9 +754,10 @@ static int buildRegistrationBody(EurekaClientSettings *settings, settings->healthCheckUrl, renewalSecs, durationSecs, - settings->version ? settings->version : "1", - settings->version ? settings->version : "1", - settings->version ? settings->version : "1" + settings->swaggerUrl ? settings->swaggerUrl : "", + version, + version, + version ); if (n <= 0 || n >= bufLen) { diff --git a/defaults.yaml b/defaults.yaml index 4b85db550..1725977be 100644 --- a/defaults.yaml +++ b/defaults.yaml @@ -37,7 +37,7 @@ components: enabled: ${{ components['caching-service']?.enabled || components.apiml?.enabled }} enabled: ${{ components.discovery?.enabled || components.apiml?.enabled }} serviceName: "zss" - static: false + static: ${{ (haInstances && Object.keys(haInstances).length > 1) ? false : true }} heartbeatIntervalSeconds: 30 retryIntervalSeconds: 10 handshakeTimeout: 30000 diff --git a/h/eurekaClient.h b/h/eurekaClient.h index 2b09fe6ed..fba97afa3 100644 --- a/h/eurekaClient.h +++ b/h/eurekaClient.h @@ -63,6 +63,7 @@ typedef struct EurekaClientSettings_tag { char *homePageUrl; /* e.g. "https://:/" */ char *statusPageUrl; /* e.g. "https://:/info" */ char *healthCheckUrl; /* e.g. "https://:/health" */ + char *swaggerUrl; /* e.g. "https://:/api-docs/agent" */ /* ---- Service version reported in Eureka metadata ---- */ const char *version; /* e.g. "2.17.0+20260101" */ From 2b6ac974f1a765f26796cac9bdadba4d1ff0aeb2 Mon Sep 17 00:00:00 2001 From: 1000TurquoisePogs Date: Wed, 11 Mar 2026 09:09:22 -0400 Subject: [PATCH 9/9] Update CHANGELOG.md Signed-off-by: 1000TurquoisePogs --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cf942846..41a9bffc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to the ZSS package will be documented in this file. ## `3.5.0` -- Enhancement: ZSS can now use eureka registration to APIML, providing improved high availability over prior static registration. Static registration is still available via new property `components.zss.agent.mediationLayer.static: true` ([#???]()) +- Enhancement: ZSS can now use eureka registration to APIML, providing improved high availability over prior static registration. Static registration is still available via new property `components.zss.agent.mediationLayer.static: true` and remains default for non-HA setups. [(#811)](https://github.com/zowe/zss/pull/811) ## `3.4.0` - Bugfix: Fixed hostname to IP address lookup for "bind-test" program. [(#801)](https://github.com/zowe/zss/pull/801) @@ -12,8 +12,8 @@ All notable changes to the ZSS package will be documented in this file. - Enhancement: Utility "zis-test" is now used to ensure that ZIS is running and accessible by Zowe before starting ZSS. (zowe/zss#764) - Enhancement: Utility "bind-test" is now available in Zowe and used to validate if each Zowe server can succeed in binding to the user requested TCPIP port at each Zowe startup. (zowe/zss#764) - Enhancement: zss handles the setting "components.apiml.enabled: true" as an alternative to enabling "gateway", "discovery", and "caching-service" components. [(#787)](https://github.com/zowe/zss/pull/787) -- Bugfix: JWK logic for single-sign-on to the APIML Gateway had the potential for a high-cpu loop when AT-TLS was enabled and the destination had read errors ([#772](https://github.com/zowe/zss/pull/772)) -- Bugfix: Stop aborting when Zowe starts on new zOS version([#781](https://github.com/zowe/zss/pull/781)) +- Bugfix: JWK logic for single-sign-on to the APIML Gateway had the potential for a high-cpu loop when AT-TLS was enabled and the destination had read errors [(#772)](https://github.com/zowe/zss/pull/772) +- Bugfix: Stop aborting when Zowe starts on new zOS version[(#781)](https://github.com/zowe/zss/pull/781) ## `3.2.0` - Enhancement: include the stub version in the generated HLASM stub (#743)