diff --git a/src/main/java/org/etsi/osl/controllers/giter/service/ResourceRepoService.java b/src/main/java/org/etsi/osl/controllers/giter/service/ResourceRepoService.java index 9951efcceb1271c5e78014c8d6a7442b8c5a0d89..68b31378c9656057143a5dd9c8db0a2b1ab820fe 100644 --- a/src/main/java/org/etsi/osl/controllers/giter/service/ResourceRepoService.java +++ b/src/main/java/org/etsi/osl/controllers/giter/service/ResourceRepoService.java @@ -104,7 +104,18 @@ public class ResourceRepoService { // Step 1: Validate Custom Resource against CRD schema validateCustomResource(cr, messages, operationType); - // Step 2: Write Custom Resource to Git repository (filename: name-resourceid.yaml) + // Step 2: Pull latest changes from remote before writing + try { + gitAdapter.pull(); + log.info("Pulled latest changes from remote before {}", operationType); + } catch (Exception pullException) { + log.warn("Failed to pull latest changes before {}: {}. Proceeding anyway.", + operationType, pullException.getMessage()); + messages.append(String.format("Warning: Pull failed (%s), proceeding with local state. ", + pullException.getMessage())); + } + + // Step 3: Write Custom Resource to Git repository (filename: name-resourceid.yaml) crFilePath = gitAdapter.writeCustomResource(cr, resourceid); String s = String.format("Custom Resource %s in Git - Path: %s", @@ -112,7 +123,7 @@ public class ResourceRepoService { messages.append(s); log.info(s); - // Step 3: Commit changes + // Step 4: Commit changes String commitMessage = String.format("[%s] %s %s-%s", operationType, cr.getKind(), cr.getMetadata().getName(), resourceid); String commitId = gitAdapter.commit(commitMessage, resourceid); @@ -121,12 +132,10 @@ public class ResourceRepoService { messages.append(s); log.info(s); - // Step 4: Push to remote - gitAdapter.push(); - log.info("Changes pushed to remote"); - messages.append(" Pushed to remote."); + // Step 5: Push to remote (with retry on conflict) + pushWithRetry(messages); - // Step 5: Handle reconciliation + // Step 6: Handle reconciliation handleReconciliation(operationType, resourceid, cr, crFilePath, messages); s = String.format(" %s operation completed successfully", operationType); @@ -226,6 +235,57 @@ public class ResourceRepoService { } } } + + /** + * Push to remote with retry on conflict. + * If push fails due to non-fast-forward, pulls latest changes and retries. + */ + private void pushWithRetry(StringBuilder messages) throws IOException { + int maxRetries = 3; + int retryCount = 0; + + while (retryCount < maxRetries) { + try { + gitAdapter.push(); + log.info("Changes pushed to remote"); + messages.append(" Pushed to remote."); + return; // Success, exit loop + } catch (Exception pushException) { + retryCount++; + String errorMsg = pushException.getMessage() != null ? pushException.getMessage() : "Unknown error"; + + // Check if it's a non-fast-forward error (conflict) + if (errorMsg.contains("non-fast-forward") || errorMsg.contains("rejected") || + errorMsg.contains("failed to push") || errorMsg.contains("fetch first")) { + + if (retryCount < maxRetries) { + log.warn("Push failed (attempt {}/{}): {}. Pulling and retrying...", + retryCount, maxRetries, errorMsg); + messages.append(String.format(" Push conflict (attempt %d), pulling and retrying.", retryCount)); + + try { + // Pull latest changes (this should merge/rebase) + gitAdapter.pull(); + log.info("Pulled latest changes after push conflict"); + } catch (Exception pullEx) { + log.error("Failed to pull after push conflict: {}", pullEx.getMessage()); + messages.append(String.format(" Pull after conflict failed: %s.", pullEx.getMessage())); + throw new IOException("Push conflict and unable to resolve: " + pullEx.getMessage(), pullEx); + } + } else { + log.error("Push failed after {} retries: {}", maxRetries, errorMsg); + messages.append(String.format(" Push failed after %d retries: %s.", maxRetries, errorMsg)); + throw new IOException("Push failed after " + maxRetries + " retries: " + errorMsg, pushException); + } + } else { + // Not a conflict error, fail immediately + log.error("Push failed with error: {}", errorMsg); + messages.append(String.format(" Push failed: %s.", errorMsg)); + throw new IOException("Push failed: " + errorMsg, pushException); + } + } + } + } public Resource deleteResource( Map headers, ResourceUpdate r) { String resourceid = ""; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2ba85543d0cc981519af4f1cd9399c16a5f63cf3..8e8c66cc3140749ddf1dd122514692f28a0045de 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,7 +19,7 @@ producer: status-watcher: poll-interval-ms: ${PRODUCER_STATUS_WATCHER_POLL_INTERVAL:10000} timeout-ms: ${PRODUCER_STATUS_WATCHER_TIMEOUT:30000} - persistence-path: ${PRODUCER_STATUS_WATCHER_PERSISTENCE_PATH:} + persistence-path: ${PRODUCER_STATUS_WATCHER_PERSISTENCE_PATH:/tmp/.} default-resource-status-type: ${PRODUCER_STATUS_WATCHER_DEFAULT_RESOURCE_STATUS_TYPE:UNKNOWN} default-operational-state-type: ${PRODUCER_STATUS_WATCHER_DEFAULT_OPERATIONAL_STATE_TYPE:DISABLE} status-mappings: