From ddf6b0d3209f64ce1944f6638453027bfd2dec84 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 31 Oct 2025 09:45:28 -0600 Subject: [PATCH] fix target idempotency, bump binary versions Signed-off-by: Travis Glenn Hansen --- Dockerfile | 8 ++-- Dockerfile.Windows | 8 ++-- src/driver/freenas/api.js | 77 +++++++++++++++++++++++++++++---- src/driver/freenas/http/api.js | 2 +- src/driver/freenas/ssh.js | 79 ++++++++++++++++++++++++++++++---- 5 files changed, 148 insertions(+), 26 deletions(-) diff --git a/Dockerfile b/Dockerfile index 28bab3d..16817ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -121,19 +121,19 @@ RUN \ echo '83e7a026-2564-455b-ada6-ddbdaf0bc519' > /etc/nvme/hostid && \ echo 'nqn.2014-08.org.nvmexpress:uuid:941e4f03-2cd6-435e-86df-731b1c573d86' > /etc/nvme/hostnqn -ARG RCLONE_VERSION=1.69.1 +ARG RCLONE_VERSION=1.71.2 ADD docker/rclone-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/rclone-installer.sh && rclone-installer.sh -ARG RESTIC_VERSION=0.18.0 +ARG RESTIC_VERSION=0.18.1 ADD docker/restic-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/restic-installer.sh && restic-installer.sh -ARG KOPIA_VERSION=0.19.0 +ARG KOPIA_VERSION=0.21.1 ADD docker/kopia-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/kopia-installer.sh && kopia-installer.sh -ARG YQ_VERSION=v4.45.1 +ARG YQ_VERSION=v4.48.1 ADD docker/yq-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/yq-installer.sh && yq-installer.sh diff --git a/Dockerfile.Windows b/Dockerfile.Windows index bb5c058..0c0c2a0 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -41,22 +41,22 @@ RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f RUN mkdir \usr\local\bin; mkdir \tmp -ARG RCLONE_VERSION=v1.69.1 +ARG RCLONE_VERSION=v1.71.2 RUN Invoke-WebRequest "https://github.com/rclone/rclone/releases/download/${env:RCLONE_VERSION}/rclone-${env:RCLONE_VERSION}-windows-amd64.zip" -OutFile '/tmp/rclone.zip' -UseBasicParsing ; \ Expand-Archive C:\tmp\rclone.zip -DestinationPath C:\tmp ; \ Copy-Item $('C:\tmp\rclone-{0}-windows-amd64\rclone.exe' -f $env:RCLONE_VERSION) -Destination "C:\usr\local\bin" -ARG RESTIC_VERSION=0.18.0 +ARG RESTIC_VERSION=0.18.1 RUN Invoke-WebRequest "https://github.com/restic/restic/releases/download/v${env:RESTIC_VERSION}/restic_${env:RESTIC_VERSION}_windows_amd64.zip" -OutFile '/tmp/restic.zip' -UseBasicParsing ; \ Expand-Archive C:\tmp\restic.zip -DestinationPath C:\tmp ; \ Copy-Item $('C:\tmp\restic_{0}_windows_amd64.exe' -f $env:RESTIC_VERSION) -Destination "C:\usr\local\bin\restic.exe" -ARG KOPIA_VERSION=0.19.0 +ARG KOPIA_VERSION=0.21.1 RUN Invoke-WebRequest "https://github.com/kopia/kopia/releases/download/v${env:KOPIA_VERSION}/kopia-${env:KOPIA_VERSION}-windows-x64.zip" -OutFile '/tmp/kopia.zip' -UseBasicParsing ; \ Expand-Archive C:\tmp\kopia.zip -DestinationPath C:\tmp ; \ Copy-Item $('C:\tmp\kopia-{0}-windows-x64\kopia.exe' -f $env:KOPIA_VERSION) -Destination "C:\usr\local\bin" -ARG YQ_VERSION=v4.45.1 +ARG YQ_VERSION=v4.48.1 RUN Invoke-WebRequest "https://github.com/mikefarah/yq/releases/download/${env:YQ_VERSION}/yq_windows_amd64.zip" -OutFile '/tmp/yq.zip' -UseBasicParsing ; \ Expand-Archive C:\tmp\yq.zip -DestinationPath C:\tmp ; \ Copy-Item $('C:\tmp\yq_windows_amd64.exe') -Destination "C:\usr\local\bin\yq.exe" diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 65cf970..9c9395d 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -178,6 +178,49 @@ class FreeNASApiDriver extends CsiBaseDriver { }); } + /** + * Check if an error response indicates a target already exists. + * This method handles variations in TrueNAS API error messages across different API versions. + * + * @param {string|Object} responseBody - The HTTP response body (string or object) + * @returns {boolean} - true if the error indicates target already exists + */ + isTargetAlreadyExistsError(responseBody) { + // Extract error message more efficiently + let errorString = ""; + + if (typeof responseBody === "string") { + errorString = responseBody; + } else if (responseBody && typeof responseBody === "object") { + // Try common error message fields first to avoid full JSON.stringify + errorString = + responseBody.message || + responseBody.error || + responseBody.detail || + JSON.stringify(responseBody); + } else { + return false; + } + + // Handle multiple variations of the target already exists error message + const targetExistsPatterns = [ + "Target name already exists", // Original pattern in code (API v1) + "Target with this name already exists", // Actual TrueNAS error message (API v2) + "Target\\b.*\\balready\\b.*\\bexists", // Flexible pattern with word boundaries + ]; + + return targetExistsPatterns.some((pattern) => { + if (pattern.includes("\\")) { + // Use regex for flexible patterns with word boundaries + const regex = new RegExp(pattern, "i"); + return regex.test(errorString); + } else { + // Use case-insensitive simple string matching for exact patterns + return errorString.toLowerCase().includes(pattern.toLowerCase()); + } + }); + } + /** * should create any necessary share resources * should set the SHARE_VOLUME_CONTEXT_PROPERTY_NAME propery @@ -884,21 +927,30 @@ class FreeNASApiDriver extends CsiBaseDriver { target ); - // 409 if invalid + // 409 Conflict - target already exists or other validation errors if (response.statusCode != 201) { target = null; if ( response.statusCode == 409 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) + this.isTargetAlreadyExistsError(response.body) ) { + this.ctx.logger.debug( + "iSCSI target already exists, attempting to find existing target with name: %s", + iscsiName + ); target = await httpApiClient.findResourceByProperties( "/services/iscsi/target", { iscsi_target_name: iscsiName, } ); + if (target) { + this.ctx.logger.debug( + "Found existing iSCSI target with ID: %s, name: %s", + target.id, + target.iscsi_target_name + ); + } } else { throw new GrpcError( grpc.status.UNKNOWN, @@ -1175,21 +1227,30 @@ class FreeNASApiDriver extends CsiBaseDriver { response = await httpClient.post("/iscsi/target", target); - // 409 if invalid + // 422 Unprocessable Entity - validation errors including duplicate targets if (response.statusCode != 200) { target = null; if ( response.statusCode == 422 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) + this.isTargetAlreadyExistsError(response.body) ) { + this.ctx.logger.debug( + "iSCSI target already exists, attempting to find existing target with name: %s", + iscsiName + ); target = await httpApiClient.findResourceByProperties( "/iscsi/target", { name: iscsiName, } ); + if (target) { + this.ctx.logger.debug( + "Found existing iSCSI target with ID: %s, name: %s", + target.id, + target.name + ); + } } else { throw new GrpcError( grpc.status.UNKNOWN, diff --git a/src/driver/freenas/http/api.js b/src/driver/freenas/http/api.js index 4687ce1..de78bf6 100644 --- a/src/driver/freenas/http/api.js +++ b/src/driver/freenas/http/api.js @@ -67,7 +67,7 @@ class Api { // crude stoppage attempt let response = await httpClient.get(endpoint, queryParams); if (lastReponse) { - if (JSON.stringify(lastReponse) == JSON.stringify(response)) { + if (JSON.stringify(lastReponse.body) == JSON.stringify(response.body)) { break; } } diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index ab26768..0840b1a 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -228,7 +228,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { // crude stoppage attempt let response = await httpClient.get(endpoint, queryParams); if (lastReponse) { - if (JSON.stringify(lastReponse) == JSON.stringify(response)) { + if (JSON.stringify(lastReponse.body) == JSON.stringify(response.body)) { break; } } @@ -273,6 +273,49 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { return target; } + /** + * Check if an error response indicates a target already exists. + * This method handles variations in TrueNAS API error messages across different API versions. + * + * @param {string|Object} responseBody - The HTTP response body (string or object) + * @returns {boolean} - true if the error indicates target already exists + */ + isTargetAlreadyExistsError(responseBody) { + // Extract error message more efficiently + let errorString = ""; + + if (typeof responseBody === "string") { + errorString = responseBody; + } else if (responseBody && typeof responseBody === "object") { + // Try common error message fields first to avoid full JSON.stringify + errorString = + responseBody.message || + responseBody.error || + responseBody.detail || + JSON.stringify(responseBody); + } else { + return false; + } + + // Handle multiple variations of the target already exists error message + const targetExistsPatterns = [ + "Target name already exists", // Original pattern in code (API v1) + "Target with this name already exists", // Actual TrueNAS error message (API v2) + "Target\\b.*\\balready\\b.*\\bexists", // Flexible pattern with word boundaries + ]; + + return targetExistsPatterns.some((pattern) => { + if (pattern.includes("\\")) { + // Use regex for flexible patterns with word boundaries + const regex = new RegExp(pattern, "i"); + return regex.test(errorString); + } else { + // Use case-insensitive simple string matching for exact patterns + return errorString.toLowerCase().includes(pattern.toLowerCase()); + } + }); + } + /** * should create any necessary share resources * should set the SHARE_VOLUME_CONTEXT_PROPERTY_NAME propery @@ -982,21 +1025,30 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { target ); - // 409 if invalid + // 409 Conflict - target already exists or other validation errors if (response.statusCode != 201) { target = null; if ( response.statusCode == 409 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) + this.isTargetAlreadyExistsError(response.body) ) { + this.ctx.logger.debug( + "iSCSI target already exists, attempting to find existing target with name: %s", + iscsiName + ); target = await this.findResourceByProperties( "/services/iscsi/target", { iscsi_target_name: iscsiName, } ); + if (target) { + this.ctx.logger.debug( + "Found existing iSCSI target with ID: %s, name: %s", + target.id, + target.iscsi_target_name + ); + } } else { throw new GrpcError( grpc.status.UNKNOWN, @@ -1271,21 +1323,30 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { response = await httpClient.post("/iscsi/target", target); - // 409 if invalid + // 422 Unprocessable Entity - validation errors including duplicate targets if (response.statusCode != 200) { target = null; if ( response.statusCode == 422 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) + this.isTargetAlreadyExistsError(response.body) ) { + this.ctx.logger.debug( + "iSCSI target already exists, attempting to find existing target with name: %s", + iscsiName + ); target = await this.findResourceByProperties( "/iscsi/target", { name: iscsiName, } ); + if (target) { + this.ctx.logger.debug( + "Found existing iSCSI target with ID: %s, name: %s", + target.id, + target.name + ); + } } else { throw new GrpcError( grpc.status.UNKNOWN,