Compare commits
	
		
			4 Commits
		
	
	
		
			main-rfs-c
			...
			developmen
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 0e63efda61 | ||
|  | 568a5b0a49 | ||
|  | d431705501 | ||
|  | b4e370b668 | 
| @@ -21,12 +21,8 @@ fn assert_eq(actual, expected, message) { | |||||||
|  |  | ||||||
| // Helper function to check if buildah is available | // Helper function to check if buildah is available | ||||||
| fn is_buildah_available() { | fn is_buildah_available() { | ||||||
|     try { |     let command = run("which buildah"); | ||||||
|         let result = run("which buildah"); |     return command.silent().execute().success; | ||||||
|         return result.success; |  | ||||||
|     } catch(err) { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| print("=== Testing Buildah Builder Pattern ==="); | print("=== Testing Buildah Builder Pattern ==="); | ||||||
| @@ -35,8 +31,7 @@ print("=== Testing Buildah Builder Pattern ==="); | |||||||
| let buildah_available = is_buildah_available(); | let buildah_available = is_buildah_available(); | ||||||
| if !buildah_available { | if !buildah_available { | ||||||
|     print("Buildah is not available. Skipping Buildah tests."); |     print("Buildah is not available. Skipping Buildah tests."); | ||||||
|     // Exit gracefully without error |     throw err; | ||||||
|     return; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| print("✓ Buildah is available"); | print("✓ Buildah is available"); | ||||||
| @@ -121,7 +116,7 @@ try { | |||||||
|     // Test committing to an image |     // Test committing to an image | ||||||
|     print("Testing commit()..."); |     print("Testing commit()..."); | ||||||
|     let image_name = "rhai_test_image:latest"; |     let image_name = "rhai_test_image:latest"; | ||||||
|     builder.commit(image_name); |     builder.commit(image_name, []); | ||||||
|     print("✓ commit(): Container committed to image successfully"); |     print("✓ commit(): Container committed to image successfully"); | ||||||
|      |      | ||||||
|     // Test removing the container |     // Test removing the container | ||||||
| @@ -154,19 +149,21 @@ try { | |||||||
|     // Clean up in case of error |     // Clean up in case of error | ||||||
|     try { |     try { | ||||||
|         // Remove test container if it exists |         // Remove test container if it exists | ||||||
|         run("buildah rm rhai_test_container"); |         let command = run("buildah rm rhai_test_container"); | ||||||
|     } catch(_) {} |         command.execute(); | ||||||
|  |     } catch(err) {} | ||||||
|      |      | ||||||
|     try { |     try { | ||||||
|         // Remove test image if it exists |         // Remove test image if it exists | ||||||
|         run("buildah rmi rhai_test_image:latest"); |         let command = run("buildah rmi alpine"); | ||||||
|     } catch(_) {} |         command.execute(); | ||||||
|  |     } catch(err) {} | ||||||
|      |      | ||||||
|     try { |     try { | ||||||
|         // Remove test files if they exist |         // Remove test files if they exist | ||||||
|         delete("test_add_file.txt"); |         delete("test_add_file.txt"); | ||||||
|         delete("test_copy_file.txt"); |         delete("test_copy_file.txt"); | ||||||
|     } catch(_) {} |     } catch(err) {} | ||||||
|      |      | ||||||
|     throw err; |     throw err; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,19 +21,25 @@ fn assert_eq(actual, expected, message) { | |||||||
|  |  | ||||||
| // Helper function to check if buildah is available | // Helper function to check if buildah is available | ||||||
| fn is_buildah_available() { | fn is_buildah_available() { | ||||||
|     try { |     let command = run("which buildah"); | ||||||
|         let result = run("which buildah"); |     return command.silent().execute().success; | ||||||
|         return result.success; |  | ||||||
|     } catch(err) { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Helper function to check if an image exists | // Helper function to check if an image exists | ||||||
| fn image_exists(image_name) { | fn image_exists(image_name) { | ||||||
|     try { |     try { | ||||||
|         let result = run(`buildah images -q ${image_name}`); |         // First, check for the exact image name | ||||||
|         return result.success && result.stdout.trim() != ""; |         let command = run(`buildah images -q ${image_name}`); | ||||||
|  |         let result = command.execute(); | ||||||
|  |         if result.success && result.stdout.trim() != "" { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // If not found, check for the localhost-prefixed version | ||||||
|  |         let prefixed_image_name = `localhost/${image_name}`; | ||||||
|  |         let command = run(`buildah images -q ${prefixed_image_name}`); | ||||||
|  |         let result_prefixed = command.execute(); | ||||||
|  |         return result_prefixed.success && result_prefixed.stdout.trim() != ""; | ||||||
|     } catch(err) { |     } catch(err) { | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| @@ -45,8 +51,7 @@ print("=== Testing Buildah Image Operations ==="); | |||||||
| let buildah_available = is_buildah_available(); | let buildah_available = is_buildah_available(); | ||||||
| if !buildah_available { | if !buildah_available { | ||||||
|     print("Buildah is not available. Skipping Buildah tests."); |     print("Buildah is not available. Skipping Buildah tests."); | ||||||
|     // Exit gracefully without error |     throw err; | ||||||
|     return; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| print("✓ Buildah is available"); | print("✓ Buildah is available"); | ||||||
| @@ -82,8 +87,10 @@ try { | |||||||
|      |      | ||||||
|     // Find our tagged image |     // Find our tagged image | ||||||
|     let found_tag = false; |     let found_tag = false; | ||||||
|  |     let expected_tag = "rhai_test_tag:latest"; | ||||||
|     for image in images { |     for image in images { | ||||||
|         if image.names.contains("rhai_test_tag:latest") { |         // The tag might be prefixed with 'localhost/' if no registry is specified. | ||||||
|  |         if image.names.contains(expected_tag) || image.names.contains("localhost/" + expected_tag) { | ||||||
|             found_tag = true; |             found_tag = true; | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
| @@ -95,10 +102,11 @@ try { | |||||||
|     print("Testing build()..."); |     print("Testing build()..."); | ||||||
|      |      | ||||||
|     // Create a simple Dockerfile |     // Create a simple Dockerfile | ||||||
|     let dockerfile_content = `FROM alpine:latest |     let dockerfile_content = ` | ||||||
| RUN echo "Hello from Dockerfile" > /hello.txt |         FROM alpine:latest | ||||||
| CMD ["cat", "/hello.txt"] |         RUN echo "Hello from Dockerfile" > /hello.txt | ||||||
| `; |         CMD ["cat", "/hello.txt"] | ||||||
|  |     `; | ||||||
|     file_write(`${test_dir}/Dockerfile`, dockerfile_content); |     file_write(`${test_dir}/Dockerfile`, dockerfile_content); | ||||||
|      |      | ||||||
|     // Build the image |     // Build the image | ||||||
| @@ -133,18 +141,23 @@ CMD ["cat", "/hello.txt"] | |||||||
|     // Clean up in case of error |     // Clean up in case of error | ||||||
|     try { |     try { | ||||||
|         // Remove test container if it exists |         // Remove test container if it exists | ||||||
|         run("buildah rm rhai_test_container"); |         let command = run("buildah rm rhai_test_container"); | ||||||
|     } catch(_) {} |         command.execute(); | ||||||
|  |     } catch(err) {} | ||||||
|      |      | ||||||
|     try { |     try { | ||||||
|         // Remove test images if they exist |         // Remove test images if they exist | ||||||
|         run("buildah rmi rhai_test_tag:latest"); |         let command = run("buildah rmi rhai_test_tag:latest"); | ||||||
|         run("buildah rmi rhai_test_build:latest"); |         command.execute(); | ||||||
|     } catch(_) {} |         let command = run("buildah rmi rhai_test_build:latest"); | ||||||
|  |         command.execute(); | ||||||
|  |     } catch(err) {} | ||||||
|  |  | ||||||
|     throw err; |     try { | ||||||
| } finally { |         // Remove test directory if it exists | ||||||
|     // Clean up test directory |  | ||||||
|         delete(test_dir); |         delete(test_dir); | ||||||
|         print("✓ Cleanup: Test directory removed"); |         print("✓ Cleanup: Test directory removed"); | ||||||
|  |     } catch (err) {} | ||||||
|  |      | ||||||
|  |     throw err; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,12 +21,8 @@ fn assert_eq(actual, expected, message) { | |||||||
|  |  | ||||||
| // Helper function to check if buildah is available | // Helper function to check if buildah is available | ||||||
| fn is_buildah_available() { | fn is_buildah_available() { | ||||||
|     try { |     let command = run("which buildah"); | ||||||
|         let result = run("which buildah"); |     return command.silent().execute().success; | ||||||
|         return result.success; |  | ||||||
|     } catch(err) { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| print("=== Testing Buildah Container Operations ==="); | print("=== Testing Buildah Container Operations ==="); | ||||||
| @@ -35,8 +31,7 @@ print("=== Testing Buildah Container Operations ==="); | |||||||
| let buildah_available = is_buildah_available(); | let buildah_available = is_buildah_available(); | ||||||
| if !buildah_available { | if !buildah_available { | ||||||
|     print("Buildah is not available. Skipping Buildah tests."); |     print("Buildah is not available. Skipping Buildah tests."); | ||||||
|     // Exit gracefully without error |     throw err; | ||||||
|     return; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| print("✓ Buildah is available"); | print("✓ Buildah is available"); | ||||||
| @@ -59,10 +54,12 @@ try { | |||||||
|      |      | ||||||
|     // Test config |     // Test config | ||||||
|     print("Testing config()..."); |     print("Testing config()..."); | ||||||
|     let config_options = #{ |     let config_options = [ | ||||||
|         "LABEL": "rhai_test=true", |         ["label", "rhai_test_true"], | ||||||
|         "ENV": "TEST_VAR=test_value" |         ["env", "TEST_VAR=test_value"], | ||||||
|     }; |         ["env", "ANOTHER_VAR=another_value"], | ||||||
|  |         ["author", "Rhai Test With Spaces"] | ||||||
|  |     ]; | ||||||
|     builder.config(config_options); |     builder.config(config_options); | ||||||
|     print("✓ config(): Container configured successfully"); |     print("✓ config(): Container configured successfully"); | ||||||
|      |      | ||||||
| @@ -77,9 +74,10 @@ try { | |||||||
|     print("Testing content operations..."); |     print("Testing content operations..."); | ||||||
|      |      | ||||||
|     // Write content to a file |     // Write content to a file | ||||||
|     let script_content = `#!/bin/sh |     let script_content = ` | ||||||
| echo "Hello from script" |         #!/bin/sh | ||||||
| `; |         echo "Hello from script" | ||||||
|  |     `; | ||||||
|     builder.write_content(script_content, "/script.sh"); |     builder.write_content(script_content, "/script.sh"); | ||||||
|      |      | ||||||
|     // Make the script executable |     // Make the script executable | ||||||
| @@ -91,14 +89,10 @@ echo "Hello from script" | |||||||
|     assert_true(script_result.stdout.contains("Hello from script"), "Script output should contain expected text"); |     assert_true(script_result.stdout.contains("Hello from script"), "Script output should contain expected text"); | ||||||
|     print("✓ Content operations: Script created and executed successfully"); |     print("✓ Content operations: Script created and executed successfully"); | ||||||
|      |      | ||||||
|     // Test commit with config |     // Test commit | ||||||
|     print("Testing commit with config..."); |     print("Testing commit..."); | ||||||
|     let commit_options = #{ |     builder.commit("rhai_test_commit:latest", [["q", ""]]); | ||||||
|         "author": "Rhai Test", |     print("✓ commit(): Container committed successfully"); | ||||||
|         "message": "Test commit" |  | ||||||
|     }; |  | ||||||
|     builder.commit("rhai_test_commit:latest", commit_options); |  | ||||||
|     print("✓ commit(): Container committed with config successfully"); |  | ||||||
|      |      | ||||||
|     // Clean up |     // Clean up | ||||||
|     builder.remove(); |     builder.remove(); | ||||||
| @@ -115,13 +109,15 @@ echo "Hello from script" | |||||||
|     // Clean up in case of error |     // Clean up in case of error | ||||||
|     try { |     try { | ||||||
|         // Remove test container if it exists |         // Remove test container if it exists | ||||||
|         run("buildah rm rhai_test_container"); |         let command = run("buildah rm rhai_test_container"); | ||||||
|     } catch(_) {} |         command.execute(); | ||||||
|  |     } catch(err) {} | ||||||
|      |      | ||||||
|     try { |     try { | ||||||
|         // Remove test image if it exists |         // Remove test image if it exists | ||||||
|         run("buildah rmi rhai_test_commit:latest"); |         let command = run("buildah rmi rhai_test_commit:latest"); | ||||||
|     } catch(_) {} |         command.execute(); | ||||||
|  |     } catch(err) {} | ||||||
|      |      | ||||||
|     throw err; |     throw err; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,155 +0,0 @@ | |||||||
| // run_all_tests.rhai |  | ||||||
| // Runs all Buildah module tests |  | ||||||
|  |  | ||||||
| print("=== Running Buildah Module Tests ==="); |  | ||||||
|  |  | ||||||
| // Custom assert function |  | ||||||
| fn assert_true(condition, message) { |  | ||||||
|     if !condition { |  | ||||||
|         print(`ASSERTION FAILED: ${message}`); |  | ||||||
|         throw message; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Helper function to check if buildah is available |  | ||||||
| fn is_buildah_available() { |  | ||||||
|     try { |  | ||||||
|         let result = run("which buildah"); |  | ||||||
|         return result.success; |  | ||||||
|     } catch(e) { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Run each test directly |  | ||||||
| let passed = 0; |  | ||||||
| let failed = 0; |  | ||||||
| let skipped = 0; |  | ||||||
| let total = 0; |  | ||||||
|  |  | ||||||
| // Check if buildah is available |  | ||||||
| let buildah_available = is_buildah_available(); |  | ||||||
| if !buildah_available { |  | ||||||
|     print("Buildah is not available. Skipping all Buildah tests."); |  | ||||||
|     skipped = 3; // Skip all three tests |  | ||||||
|     total = 3; |  | ||||||
| } else { |  | ||||||
|     // Test 1: Builder Pattern |  | ||||||
|     print("\n--- Running Builder Pattern Tests ---"); |  | ||||||
|     try { |  | ||||||
|         // Create a builder |  | ||||||
|         let builder = bah_new("rhai_test_container", "alpine:latest"); |  | ||||||
|          |  | ||||||
|         // Test basic properties |  | ||||||
|         assert_true(builder.container_id != "", "Container ID should not be empty"); |  | ||||||
|         assert_true(builder.name == "rhai_test_container", "Container name should match"); |  | ||||||
|          |  | ||||||
|         // Run a simple command |  | ||||||
|         let result = builder.run("echo 'Hello from container'"); |  | ||||||
|         assert_true(result.success, "Command should succeed"); |  | ||||||
|          |  | ||||||
|         // Clean up |  | ||||||
|         builder.remove(); |  | ||||||
|          |  | ||||||
|         print("--- Builder Pattern Tests completed successfully ---"); |  | ||||||
|         passed += 1; |  | ||||||
|     } catch(err) { |  | ||||||
|         print(`!!! Error in Builder Pattern Tests: ${err}`); |  | ||||||
|         failed += 1; |  | ||||||
|          |  | ||||||
|         // Clean up in case of error |  | ||||||
|         try { |  | ||||||
|             run("buildah rm rhai_test_container"); |  | ||||||
|         } catch(e) { |  | ||||||
|             // Ignore errors during cleanup |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     total += 1; |  | ||||||
|      |  | ||||||
|     // Test 2: Image Operations |  | ||||||
|     print("\n--- Running Image Operations Tests ---"); |  | ||||||
|     try { |  | ||||||
|         // Create a temporary directory for testing |  | ||||||
|         let test_dir = "rhai_test_buildah"; |  | ||||||
|         mkdir(test_dir); |  | ||||||
|          |  | ||||||
|         // Create a builder |  | ||||||
|         let builder = bah_new("rhai_test_container", "alpine:latest"); |  | ||||||
|          |  | ||||||
|         // List images |  | ||||||
|         let images = builder.images(); |  | ||||||
|         assert_true(images.len() > 0, "There should be at least one image"); |  | ||||||
|          |  | ||||||
|         // Clean up |  | ||||||
|         builder.remove(); |  | ||||||
|         delete(test_dir); |  | ||||||
|          |  | ||||||
|         print("--- Image Operations Tests completed successfully ---"); |  | ||||||
|         passed += 1; |  | ||||||
|     } catch(err) { |  | ||||||
|         print(`!!! Error in Image Operations Tests: ${err}`); |  | ||||||
|         failed += 1; |  | ||||||
|          |  | ||||||
|         // Clean up in case of error |  | ||||||
|         try { |  | ||||||
|             run("buildah rm rhai_test_container"); |  | ||||||
|             delete("rhai_test_buildah"); |  | ||||||
|         } catch(e) { |  | ||||||
|             // Ignore errors during cleanup |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     total += 1; |  | ||||||
|      |  | ||||||
|     // Test 3: Container Operations |  | ||||||
|     print("\n--- Running Container Operations Tests ---"); |  | ||||||
|     try { |  | ||||||
|         // Create a builder |  | ||||||
|         let builder = bah_new("rhai_test_container", "alpine:latest"); |  | ||||||
|          |  | ||||||
|         // Test reset |  | ||||||
|         builder.reset(); |  | ||||||
|          |  | ||||||
|         // Create a new container |  | ||||||
|         builder = bah_new("rhai_test_container", "alpine:latest"); |  | ||||||
|          |  | ||||||
|         // Run a command |  | ||||||
|         let result = builder.run("echo 'Hello from container'"); |  | ||||||
|         assert_true(result.success, "Command should succeed"); |  | ||||||
|          |  | ||||||
|         // Clean up |  | ||||||
|         builder.remove(); |  | ||||||
|          |  | ||||||
|         print("--- Container Operations Tests completed successfully ---"); |  | ||||||
|         passed += 1; |  | ||||||
|     } catch(err) { |  | ||||||
|         print(`!!! Error in Container Operations Tests: ${err}`); |  | ||||||
|         failed += 1; |  | ||||||
|          |  | ||||||
|         // Clean up in case of error |  | ||||||
|         try { |  | ||||||
|             run("buildah rm rhai_test_container"); |  | ||||||
|         } catch(e) { |  | ||||||
|             // Ignore errors during cleanup |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     total += 1; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| print("\n=== Test Summary ==="); |  | ||||||
| print(`Passed: ${passed}`); |  | ||||||
| print(`Failed: ${failed}`); |  | ||||||
| print(`Skipped: ${skipped}`); |  | ||||||
| print(`Total: ${total}`); |  | ||||||
|  |  | ||||||
| if failed == 0 { |  | ||||||
|     if skipped > 0 { |  | ||||||
|         print("\n⚠️ All tests skipped or passed!"); |  | ||||||
|     } else { |  | ||||||
|         print("\n✅ All tests passed!"); |  | ||||||
|     } |  | ||||||
| } else { |  | ||||||
|     print("\n❌ Some tests failed!"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Return the number of failed tests (0 means success) |  | ||||||
| failed; |  | ||||||
| @@ -21,22 +21,31 @@ fn assert_eq(actual, expected, message) { | |||||||
|  |  | ||||||
| // Helper function to check if nerdctl is available | // Helper function to check if nerdctl is available | ||||||
| fn is_nerdctl_available() { | fn is_nerdctl_available() { | ||||||
|     try { |     let command = run("which nerdctl"); | ||||||
|         let result = run("which nerdctl"); |     return command.silent().execute().success; | ||||||
|         return result.success; |  | ||||||
|     } catch(err) { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Helper function to check if a container exists | // Helper function to check if a container exists | ||||||
| fn container_exists(container_name) { | fn container_exists(container_name) { | ||||||
|     try { |     // let command = run(`nerdctl ps -a --format "{{.Names}}" | grep -w ${container_name}`); | ||||||
|         let result = run(`nerdctl ps -a --format "{{.Names}}" | grep -w ${container_name}`); |     let command = run(`nerdctl ps -a --format "{{.Names}}"`); | ||||||
|         return result.success; |     let result = command.silent().execute(); | ||||||
|     } catch(err) { |  | ||||||
|  |     // Check if the command was successful | ||||||
|  |     if !result.success { | ||||||
|  |         print(`Error executing 'nerdctl ps': ${result.stderr}`); | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Split the output into individual lines (names) | ||||||
|  |     // and check if any of them is an exact match for our container name. | ||||||
|  |     for line in result.stdout.split('\n') { | ||||||
|  |         if line.trim() == container_name { | ||||||
|  |             return true; // Found the container | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return false; // Did not find the container | ||||||
| } | } | ||||||
|  |  | ||||||
| // Helper function to clean up a container if it exists | // Helper function to clean up a container if it exists | ||||||
| @@ -49,6 +58,8 @@ fn cleanup_container(container_name) { | |||||||
|         } catch(err) { |         } catch(err) { | ||||||
|             print(`Error cleaning up container ${container_name}: ${err}`); |             print(`Error cleaning up container ${container_name}: ${err}`); | ||||||
|         } |         } | ||||||
|  |     } else { | ||||||
|  |         print!(`No container with name ${container_name} found. Nothing to clean up.`); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -58,8 +69,7 @@ print("=== Testing Nerdctl Container Operations ==="); | |||||||
| let nerdctl_available = is_nerdctl_available(); | let nerdctl_available = is_nerdctl_available(); | ||||||
| if !nerdctl_available { | if !nerdctl_available { | ||||||
|     print("nerdctl is not available. Skipping Nerdctl tests."); |     print("nerdctl is not available. Skipping Nerdctl tests."); | ||||||
|     // Exit gracefully without error |     throw err; | ||||||
|     return; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| print("✓ nerdctl is available"); | print("✓ nerdctl is available"); | ||||||
| @@ -81,84 +91,132 @@ try { | |||||||
|     assert_eq(container.container_id, "", "Container ID should be empty initially"); |     assert_eq(container.container_id, "", "Container ID should be empty initially"); | ||||||
|      |      | ||||||
|     // Test setting container image |     // Test setting container image | ||||||
|     print("Testing with_image()..."); |     print("Testing image setter...");  | ||||||
|     container.with_image("alpine:latest"); |     container.image = "alpine:latest"; | ||||||
|     assert_eq(container.image, "alpine:latest", "Container image should match"); |     assert_eq(container.image, "alpine:latest", "Container image should match"); | ||||||
|  |  | ||||||
|     // Test setting detach mode |     // Test setting container config | ||||||
|     print("Testing with_detach()..."); |     print("Testing config setter..."); | ||||||
|     container.with_detach(true); |     let config_options = #{"key1": "value1", "key2": "value2"}; | ||||||
|     assert_true(container.detach, "Container detach mode should be true"); |     container.config = config_options; | ||||||
|  |     assert_eq(container.config, config_options, "Container config options should match"); | ||||||
|  |  | ||||||
|     // Test setting environment variables |     // Test container_id setter and getter | ||||||
|     print("Testing with_env()..."); |     print("Testing container_id setter..."); | ||||||
|     container.with_env("TEST_VAR", "test_value"); |     container.container_id = "test-id"; | ||||||
|  |     assert_eq(container.container_id, "test-id", "Container ID should be 'test-id'"); | ||||||
|      |      | ||||||
|     // Test setting multiple environment variables |     // Test ports setter and getter | ||||||
|     print("Testing with_envs()..."); |     print("Testing ports setter and getter..."); | ||||||
|     let env_map = #{ |     let ports_list = ["1234", "2345"]; | ||||||
|         "VAR1": "value1", |     container.ports = ports_list; | ||||||
|         "VAR2": "value2" |     assert_eq(container.ports, ports_list, "Container ports should match"); | ||||||
|     }; |  | ||||||
|     container.with_envs(env_map); |  | ||||||
|  |  | ||||||
|     // Test setting ports |     // Test volumes setter and getter | ||||||
|     print("Testing with_port()..."); |     print("Testing volumes setter and getter..."); | ||||||
|     container.with_port("8080:80"); |     let volumes_list = ["/tmp:/tmp"]; | ||||||
|  |     container.volumes = volumes_list; | ||||||
|  |     assert_eq(container.volumes, volumes_list, "Container volumes should match"); | ||||||
|  |  | ||||||
|     // Test setting multiple ports |     // Test env_vars setter and getter | ||||||
|     print("Testing with_ports()..."); |     print("Testing env_vars setter and getter..."); | ||||||
|     container.with_ports(["9090:90", "7070:70"]); |     let env_vars_map = #{"VAR1": "value1", "VAR2": "value2"}; | ||||||
|  |     container.env_vars = env_vars_map; | ||||||
|  |     assert_eq(container.env_vars, env_vars_map, "Container env_vars should match"); | ||||||
|  |  | ||||||
|     // Test setting volumes |     // Test network setter and getter | ||||||
|     print("Testing with_volume()..."); |     print("Testing network setter and getter..."); | ||||||
|     // Create a test directory for volume mounting |     container.network = "test-net"; | ||||||
|     let test_dir = "rhai_test_nerdctl_volume"; |     assert_eq(container.network, "test-net", "Container network should match"); | ||||||
|     mkdir(test_dir); |  | ||||||
|     container.with_volume(`${test_dir}:/data`); |  | ||||||
|  |  | ||||||
|     // Test setting resource limits |     // Test network_aliases setter and getter | ||||||
|     print("Testing with_cpu_limit() and with_memory_limit()..."); |     print("Testing network_aliases setter and getter..."); | ||||||
|     container.with_cpu_limit("0.5"); |     let aliases = ["alias1", "alias2"]; | ||||||
|     container.with_memory_limit("256m"); |     container.network_aliases = aliases; | ||||||
|  |     assert_eq(container.network_aliases, aliases, "Container network_aliases should match"); | ||||||
|  |  | ||||||
|     // Test running the container |     // Test cpu_limit setter and getter | ||||||
|     print("Testing run()..."); |     print("Testing cpu_limit setter and getter..."); | ||||||
|     let run_result = container.run(); |     container.cpu_limit = "0.5"; | ||||||
|     assert_true(run_result.success, "Container run should succeed"); |     assert_eq(container.cpu_limit, "0.5", "Container cpu_limit should match"); | ||||||
|     assert_true(container.container_id != "", "Container ID should not be empty after run"); |  | ||||||
|     print(`✓ run(): Container started with ID: ${container.container_id}`); |  | ||||||
|  |  | ||||||
|     // Test executing a command in the container |     // Test memory_limit setter and getter | ||||||
|     print("Testing exec()..."); |     print("Testing memory_limit setter and getter..."); | ||||||
|     let exec_result = container.exec("echo 'Hello from container'"); |     container.memory_limit = "512m"; | ||||||
|     assert_true(exec_result.success, "Container exec should succeed"); |     assert_eq(container.memory_limit, "512m", "Container memory_limit should match"); | ||||||
|     assert_true(exec_result.stdout.contains("Hello from container"), "Exec output should contain expected text"); |  | ||||||
|     print("✓ exec(): Command executed successfully"); |  | ||||||
|  |  | ||||||
|     // Test getting container logs |     // Test memory_swap_limit setter and getter | ||||||
|     print("Testing logs()..."); |     print("Testing memory_swap_limit setter and getter..."); | ||||||
|     let logs_result = container.logs(); |     container.memory_swap_limit = "1g"; | ||||||
|     assert_true(logs_result.success, "Container logs should succeed"); |     assert_eq(container.memory_swap_limit, "1g", "Container memory_swap_limit should match"); | ||||||
|     print("✓ logs(): Logs retrieved successfully"); |  | ||||||
|  |  | ||||||
|     // Test stopping the container |     // Test cpu_shares setter and getter | ||||||
|     print("Testing stop()..."); |     print("Testing cpu_shares setter and getter..."); | ||||||
|     let stop_result = container.stop(); |     container.cpu_shares = "1024"; | ||||||
|     assert_true(stop_result.success, "Container stop should succeed"); |     assert_eq(container.cpu_shares, "1024", "Container cpu_shares should match"); | ||||||
|     print("✓ stop(): Container stopped successfully"); |  | ||||||
|  |  | ||||||
|     // Test removing the container |     // Test restart_policy setter and getter | ||||||
|     print("Testing remove()..."); |     print("Testing restart_policy setter and getter..."); | ||||||
|     let remove_result = container.remove(); |     container.restart_policy = "always"; | ||||||
|     assert_true(remove_result.success, "Container remove should succeed"); |     assert_eq(container.restart_policy, "always", "Container restart_policy should match"); | ||||||
|     print("✓ remove(): Container removed successfully"); |  | ||||||
|  |  | ||||||
|     // Clean up test directory |     // Test detach setter and getter | ||||||
|     delete(test_dir); |     print("Testing detach setter and getter..."); | ||||||
|     print("✓ Cleanup: Test directory removed"); |     container.detach = false; | ||||||
|  |     assert_eq(container.detach, false, "Container detach should be false"); | ||||||
|  |     container.detach = true; | ||||||
|  |     assert_eq(container.detach, true, "Container detach should be true"); | ||||||
|  |  | ||||||
|     print("All container operations tests completed successfully!"); |     // Test health_check setter and getter | ||||||
|  |     print("Testing health_check setter and getter..."); | ||||||
|  |     let health_check_new = health_check_new("example_cmd"); | ||||||
|  |     container.health_check = health_check_new; | ||||||
|  |     container.health_check.interval = "example_interval"; | ||||||
|  |     assert_eq(container.health_check.cmd, "example_cmd", "Container health check cmd should match"); | ||||||
|  |     assert_eq(container.health_check.interval, "example_interval", "Container health check interval should match"); | ||||||
|  |  | ||||||
|  |     // Test snapshotter setter and getter | ||||||
|  |     print("Testing snapshotter setter and getter..."); | ||||||
|  |     container.snapshotter = "stargz"; | ||||||
|  |     assert_eq(container.snapshotter, "stargz", "Container snapshotter should match"); | ||||||
|  |  | ||||||
|  |     // // Test running the container | ||||||
|  |     // print("Testing run()..."); | ||||||
|  |     // let run_result = container.run(); | ||||||
|  |     // assert_true(run_result.success, "Container run should succeed"); | ||||||
|  |     // assert_true(container.container_id != "", "Container ID should not be empty after run"); | ||||||
|  |     // print(`✓ run(): Container started with ID: ${container.container_id}`); | ||||||
|  |      | ||||||
|  |     // // Test executing a command in the container | ||||||
|  |     // print("Testing exec()..."); | ||||||
|  |     // let exec_result = container.exec("echo 'Hello from container'"); | ||||||
|  |     // assert_true(exec_result.success, "Container exec should succeed"); | ||||||
|  |     // assert_true(exec_result.stdout.contains("Hello from container"), "Exec output should contain expected text"); | ||||||
|  |     // print("✓ exec(): Command executed successfully"); | ||||||
|  |      | ||||||
|  |     // // Test getting container logs | ||||||
|  |     // print("Testing logs()..."); | ||||||
|  |     // let logs_result = container.logs(); | ||||||
|  |     // assert_true(logs_result.success, "Container logs should succeed"); | ||||||
|  |     // print("✓ logs(): Logs retrieved successfully"); | ||||||
|  |      | ||||||
|  |     // // Test stopping the container | ||||||
|  |     // print("Testing stop()..."); | ||||||
|  |     // let stop_result = container.stop(); | ||||||
|  |     // assert_true(stop_result.success, "Container stop should succeed"); | ||||||
|  |     // print("✓ stop(): Container stopped successfully"); | ||||||
|  |      | ||||||
|  |     // // Test removing the container | ||||||
|  |     // print("Testing remove()..."); | ||||||
|  |     // let remove_result = container.remove(); | ||||||
|  |     // assert_true(remove_result.success, "Container remove should succeed"); | ||||||
|  |     // print("✓ remove(): Container removed successfully"); | ||||||
|  |      | ||||||
|  |     // // Clean up test directory | ||||||
|  |     // delete(test_dir); | ||||||
|  |     // print("✓ Cleanup: Test directory removed"); | ||||||
|  |      | ||||||
|  |     // print("All container operations tests completed successfully!"); | ||||||
| } catch(err) { | } catch(err) { | ||||||
|     print(`Error: ${err}`); |     print(`Error: ${err}`); | ||||||
|      |      | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								service_manager/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								service_manager/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | [package] | ||||||
|  | name = "sal-service-manager" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
|  | async-trait = "0.1" | ||||||
|  | thiserror = "1.0" | ||||||
|  | tokio = { workspace = true } | ||||||
|  | log = { workspace = true } | ||||||
|  | serde = { workspace = true } | ||||||
|  | serde_json = { workspace = true, optional = true } | ||||||
|  |  | ||||||
|  | zinit_client = { package = "sal-zinit-client", path = "../zinit_client", optional = true } | ||||||
|  |  | ||||||
|  | [target.'cfg(target_os = "macos")'.dependencies] | ||||||
|  | # macOS-specific dependencies for launchctl | ||||||
|  | plist = "1.6" | ||||||
|  |  | ||||||
|  | [features] | ||||||
|  | default = [] | ||||||
|  | zinit = ["dep:zinit_client", "dep:serde_json"] | ||||||
							
								
								
									
										54
									
								
								service_manager/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								service_manager/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | # Service Manager | ||||||
|  |  | ||||||
|  | This crate provides a unified interface for managing system services across different platforms. | ||||||
|  | It abstracts the underlying service management system (like `launchctl` on macOS or `systemd` on Linux), | ||||||
|  | allowing you to start, stop, and monitor services with a consistent API. | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |  | ||||||
|  | - A `ServiceManager` trait defining a common interface for service operations. | ||||||
|  | - Platform-specific implementations for: | ||||||
|  |   - macOS (`launchctl`) | ||||||
|  |   - Linux (`systemd`) | ||||||
|  | - A factory function `create_service_manager` that returns the appropriate manager for the current platform. | ||||||
|  |  | ||||||
|  | ## Usage | ||||||
|  |  | ||||||
|  | Add this to your `Cargo.toml`: | ||||||
|  |  | ||||||
|  | ```toml | ||||||
|  | [dependencies] | ||||||
|  | service_manager = { path = "../service_manager" } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Here is an example of how to use the `ServiceManager`: | ||||||
|  |  | ||||||
|  | ```rust,no_run | ||||||
|  | use service_manager::{create_service_manager, ServiceConfig}; | ||||||
|  | use std::collections::HashMap; | ||||||
|  |  | ||||||
|  | fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||||
|  |     let service_manager = create_service_manager(); | ||||||
|  |  | ||||||
|  |     let config = ServiceConfig { | ||||||
|  |         name: "my-service".to_string(), | ||||||
|  |         binary_path: "/usr/local/bin/my-service-executable".to_string(), | ||||||
|  |         args: vec!["--config".to_string(), "/etc/my-service.conf".to_string()], | ||||||
|  |         working_directory: Some("/var/tmp".to_string()), | ||||||
|  |         environment: HashMap::new(), | ||||||
|  |         auto_restart: true, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Start a new service | ||||||
|  |     service_manager.start(&config)?; | ||||||
|  |  | ||||||
|  |     // Get the status of the service | ||||||
|  |     let status = service_manager.status("my-service")?; | ||||||
|  |     println!("Service status: {:?}", status); | ||||||
|  |  | ||||||
|  |     // Stop the service | ||||||
|  |     service_manager.stop("my-service")?; | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | ``` | ||||||
							
								
								
									
										399
									
								
								service_manager/src/launchctl.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										399
									
								
								service_manager/src/launchctl.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,399 @@ | |||||||
|  | use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use std::path::PathBuf; | ||||||
|  | use tokio::process::Command; | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct LaunchctlServiceManager { | ||||||
|  |     service_prefix: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Serialize, Deserialize)] | ||||||
|  | struct LaunchDaemon { | ||||||
|  |     #[serde(rename = "Label")] | ||||||
|  |     label: String, | ||||||
|  |     #[serde(rename = "ProgramArguments")] | ||||||
|  |     program_arguments: Vec<String>, | ||||||
|  |     #[serde(rename = "WorkingDirectory", skip_serializing_if = "Option::is_none")] | ||||||
|  |     working_directory: Option<String>, | ||||||
|  |     #[serde(rename = "EnvironmentVariables", skip_serializing_if = "Option::is_none")] | ||||||
|  |     environment_variables: Option<HashMap<String, String>>, | ||||||
|  |     #[serde(rename = "KeepAlive", skip_serializing_if = "Option::is_none")] | ||||||
|  |     keep_alive: Option<bool>, | ||||||
|  |     #[serde(rename = "RunAtLoad")] | ||||||
|  |     run_at_load: bool, | ||||||
|  |     #[serde(rename = "StandardOutPath", skip_serializing_if = "Option::is_none")] | ||||||
|  |     standard_out_path: Option<String>, | ||||||
|  |     #[serde(rename = "StandardErrorPath", skip_serializing_if = "Option::is_none")] | ||||||
|  |     standard_error_path: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl LaunchctlServiceManager { | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         Self { | ||||||
|  |             service_prefix: "tf.ourworld.circles".to_string(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn get_service_label(&self, service_name: &str) -> String { | ||||||
|  |         format!("{}.{}", self.service_prefix, service_name) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn get_plist_path(&self, service_name: &str) -> PathBuf { | ||||||
|  |         let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); | ||||||
|  |         PathBuf::from(home) | ||||||
|  |             .join("Library") | ||||||
|  |             .join("LaunchAgents") | ||||||
|  |             .join(format!("{}.plist", self.get_service_label(service_name))) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn get_log_path(&self, service_name: &str) -> PathBuf { | ||||||
|  |         let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); | ||||||
|  |         PathBuf::from(home) | ||||||
|  |             .join("Library") | ||||||
|  |             .join("Logs") | ||||||
|  |             .join("circles") | ||||||
|  |             .join(format!("{}.log", service_name)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn create_plist(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||||
|  |         let label = self.get_service_label(&config.name); | ||||||
|  |         let plist_path = self.get_plist_path(&config.name); | ||||||
|  |         let log_path = self.get_log_path(&config.name); | ||||||
|  |  | ||||||
|  |         // Ensure the LaunchAgents directory exists | ||||||
|  |         if let Some(parent) = plist_path.parent() { | ||||||
|  |             tokio::fs::create_dir_all(parent).await?; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Ensure the logs directory exists | ||||||
|  |         if let Some(parent) = log_path.parent() { | ||||||
|  |             tokio::fs::create_dir_all(parent).await?; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let mut program_arguments = vec![config.binary_path.clone()]; | ||||||
|  |         program_arguments.extend(config.args.clone()); | ||||||
|  |  | ||||||
|  |         let launch_daemon = LaunchDaemon { | ||||||
|  |             label: label.clone(), | ||||||
|  |             program_arguments, | ||||||
|  |             working_directory: config.working_directory.clone(), | ||||||
|  |             environment_variables: if config.environment.is_empty() { | ||||||
|  |                 None | ||||||
|  |             } else { | ||||||
|  |                 Some(config.environment.clone()) | ||||||
|  |             }, | ||||||
|  |             keep_alive: if config.auto_restart { Some(true) } else { None }, | ||||||
|  |             run_at_load: true, | ||||||
|  |             standard_out_path: Some(log_path.to_string_lossy().to_string()), | ||||||
|  |             standard_error_path: Some(log_path.to_string_lossy().to_string()), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         let mut plist_content = Vec::new(); | ||||||
|  |         plist::to_writer_xml(&mut plist_content, &launch_daemon) | ||||||
|  |             .map_err(|e| ServiceManagerError::Other(format!("Failed to serialize plist: {}", e)))?; | ||||||
|  |         let plist_content = String::from_utf8(plist_content) | ||||||
|  |             .map_err(|e| ServiceManagerError::Other(format!("Failed to convert plist to string: {}", e)))?; | ||||||
|  |  | ||||||
|  |         tokio::fs::write(&plist_path, plist_content).await?; | ||||||
|  |  | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn run_launchctl(&self, args: &[&str]) -> Result<String, ServiceManagerError> { | ||||||
|  |         let output = Command::new("launchctl") | ||||||
|  |             .args(args) | ||||||
|  |             .output() | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         if !output.status.success() { | ||||||
|  |             let stderr = String::from_utf8_lossy(&output.stderr); | ||||||
|  |             return Err(ServiceManagerError::Other(format!( | ||||||
|  |                 "launchctl command failed: {}", | ||||||
|  |                 stderr | ||||||
|  |             ))); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Ok(String::from_utf8_lossy(&output.stdout).to_string()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn wait_for_service_status(&self, service_name: &str, timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||||
|  |         use tokio::time::{sleep, Duration, timeout}; | ||||||
|  |          | ||||||
|  |         let timeout_duration = Duration::from_secs(timeout_secs); | ||||||
|  |         let poll_interval = Duration::from_millis(500); | ||||||
|  |          | ||||||
|  |         let result = timeout(timeout_duration, async { | ||||||
|  |             loop { | ||||||
|  |                 match self.status(service_name) { | ||||||
|  |                     Ok(ServiceStatus::Running) => { | ||||||
|  |                         return Ok(()); | ||||||
|  |                     } | ||||||
|  |                     Ok(ServiceStatus::Failed) => { | ||||||
|  |                         // Service failed, get error details from logs | ||||||
|  |                         let logs = self.logs(service_name, Some(20)).unwrap_or_default(); | ||||||
|  |                         let error_msg = if logs.is_empty() { | ||||||
|  |                             "Service failed to start (no logs available)".to_string() | ||||||
|  |                         } else { | ||||||
|  |                             // Extract error lines from logs | ||||||
|  |                             let error_lines: Vec<&str> = logs | ||||||
|  |                                 .lines() | ||||||
|  |                                 .filter(|line| line.to_lowercase().contains("error") || line.to_lowercase().contains("failed")) | ||||||
|  |                                 .take(3) | ||||||
|  |                                 .collect(); | ||||||
|  |                              | ||||||
|  |                             if error_lines.is_empty() { | ||||||
|  |                                 format!("Service failed to start. Recent logs:\n{}", | ||||||
|  |                                     logs.lines().rev().take(5).collect::<Vec<_>>().into_iter().rev().collect::<Vec<_>>().join("\n")) | ||||||
|  |                             } else { | ||||||
|  |                                 format!("Service failed to start. Errors:\n{}", error_lines.join("\n")) | ||||||
|  |                             } | ||||||
|  |                         }; | ||||||
|  |                         return Err(ServiceManagerError::StartFailed(service_name.to_string(), error_msg)); | ||||||
|  |                     } | ||||||
|  |                     Ok(ServiceStatus::Stopped) | Ok(ServiceStatus::Unknown) => { | ||||||
|  |                         // Still starting, continue polling | ||||||
|  |                         sleep(poll_interval).await; | ||||||
|  |                     } | ||||||
|  |                     Err(ServiceManagerError::ServiceNotFound(_)) => { | ||||||
|  |                         return Err(ServiceManagerError::ServiceNotFound(service_name.to_string())); | ||||||
|  |                     } | ||||||
|  |                     Err(e) => { | ||||||
|  |                         return Err(e); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }).await; | ||||||
|  |          | ||||||
|  |         match result { | ||||||
|  |             Ok(Ok(())) => Ok(()), | ||||||
|  |             Ok(Err(e)) => Err(e), | ||||||
|  |             Err(_) => Err(ServiceManagerError::StartFailed( | ||||||
|  |                 service_name.to_string(), | ||||||
|  |                 format!("Service did not start within {} seconds", timeout_secs) | ||||||
|  |             )), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[async_trait] | ||||||
|  | impl ServiceManager for LaunchctlServiceManager { | ||||||
|  |     fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> { | ||||||
|  |         let plist_path = self.get_plist_path(service_name); | ||||||
|  |         Ok(plist_path.exists()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||||
|  |         // For synchronous version, we'll use blocking operations | ||||||
|  |         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||||
|  |         rt.block_on(async { | ||||||
|  |             let label = self.get_service_label(&config.name); | ||||||
|  |              | ||||||
|  |             // Check if service is already loaded | ||||||
|  |             let list_output = self.run_launchctl(&["list"]).await?; | ||||||
|  |             if list_output.contains(&label) { | ||||||
|  |                 return Err(ServiceManagerError::ServiceAlreadyExists(config.name.clone())); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Create the plist file | ||||||
|  |             self.create_plist(config).await?; | ||||||
|  |  | ||||||
|  |             // Load the service | ||||||
|  |             let plist_path = self.get_plist_path(&config.name); | ||||||
|  |             self.run_launchctl(&["load", &plist_path.to_string_lossy()]) | ||||||
|  |                 .await | ||||||
|  |                 .map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?; | ||||||
|  |  | ||||||
|  |             Ok(()) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||||
|  |         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||||
|  |         rt.block_on(async { | ||||||
|  |             let label = self.get_service_label(service_name); | ||||||
|  |             let plist_path = self.get_plist_path(service_name); | ||||||
|  |              | ||||||
|  |             // Check if plist file exists | ||||||
|  |             if !plist_path.exists() { | ||||||
|  |                 return Err(ServiceManagerError::ServiceNotFound(service_name.to_string())); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // Check if service is already loaded and running | ||||||
|  |             let list_output = self.run_launchctl(&["list"]).await?; | ||||||
|  |             if list_output.contains(&label) { | ||||||
|  |                 // Service is loaded, check if it's running | ||||||
|  |                 match self.status(service_name)? { | ||||||
|  |                     ServiceStatus::Running => { | ||||||
|  |                         return Ok(()); // Already running, nothing to do | ||||||
|  |                     } | ||||||
|  |                     _ => { | ||||||
|  |                         // Service is loaded but not running, try to start it | ||||||
|  |                         self.run_launchctl(&["start", &label]) | ||||||
|  |                             .await | ||||||
|  |                             .map_err(|e| ServiceManagerError::StartFailed(service_name.to_string(), e.to_string()))?; | ||||||
|  |                         return Ok(()); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // Service is not loaded, load it | ||||||
|  |             self.run_launchctl(&["load", &plist_path.to_string_lossy()]) | ||||||
|  |                 .await | ||||||
|  |                 .map_err(|e| ServiceManagerError::StartFailed(service_name.to_string(), e.to_string()))?; | ||||||
|  |  | ||||||
|  |             Ok(()) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn start_and_confirm(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||||
|  |         // First start the service | ||||||
|  |         self.start(config)?; | ||||||
|  |          | ||||||
|  |         // Then wait for confirmation | ||||||
|  |         self.wait_for_service_status(&config.name, timeout_secs).await | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn run(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||||
|  |         self.start_and_confirm(config, timeout_secs).await | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn start_existing_and_confirm(&self, service_name: &str, timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||||
|  |         // First start the existing service | ||||||
|  |         self.start_existing(service_name)?; | ||||||
|  |          | ||||||
|  |         // Then wait for confirmation | ||||||
|  |         self.wait_for_service_status(service_name, timeout_secs).await | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||||
|  |         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||||
|  |         rt.block_on(async { | ||||||
|  |             let _label = self.get_service_label(service_name); | ||||||
|  |             let plist_path = self.get_plist_path(service_name); | ||||||
|  |  | ||||||
|  |             // Unload the service | ||||||
|  |             self.run_launchctl(&["unload", &plist_path.to_string_lossy()]) | ||||||
|  |                 .await | ||||||
|  |                 .map_err(|e| ServiceManagerError::StopFailed(service_name.to_string(), e.to_string()))?; | ||||||
|  |  | ||||||
|  |             Ok(()) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||||
|  |         // For launchctl, we stop and start | ||||||
|  |         if let Err(e) = self.stop(service_name) { | ||||||
|  |             // If stop fails because service doesn't exist, that's ok for restart | ||||||
|  |             if !matches!(e, ServiceManagerError::ServiceNotFound(_)) { | ||||||
|  |                 return Err(ServiceManagerError::RestartFailed(service_name.to_string(), e.to_string())); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // We need the config to restart, but we don't have it stored | ||||||
|  |         // For now, return an error - in a real implementation we might store configs | ||||||
|  |         Err(ServiceManagerError::RestartFailed( | ||||||
|  |             service_name.to_string(), | ||||||
|  |             "Restart requires re-providing service configuration".to_string(), | ||||||
|  |         )) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> { | ||||||
|  |         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||||
|  |         rt.block_on(async { | ||||||
|  |             let label = self.get_service_label(service_name); | ||||||
|  |             let plist_path = self.get_plist_path(service_name); | ||||||
|  |              | ||||||
|  |             // First check if the plist file exists | ||||||
|  |             if !plist_path.exists() { | ||||||
|  |                 return Err(ServiceManagerError::ServiceNotFound(service_name.to_string())); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             let list_output = self.run_launchctl(&["list"]).await?; | ||||||
|  |              | ||||||
|  |             if !list_output.contains(&label) { | ||||||
|  |                 return Ok(ServiceStatus::Stopped); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Get detailed status | ||||||
|  |             match self.run_launchctl(&["list", &label]).await { | ||||||
|  |                 Ok(output) => { | ||||||
|  |                     if output.contains("\"PID\" = ") { | ||||||
|  |                         Ok(ServiceStatus::Running) | ||||||
|  |                     } else if output.contains("\"LastExitStatus\" = ") { | ||||||
|  |                         Ok(ServiceStatus::Failed) | ||||||
|  |                     } else { | ||||||
|  |                         Ok(ServiceStatus::Unknown) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 Err(_) => Ok(ServiceStatus::Stopped), | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn logs(&self, service_name: &str, lines: Option<usize>) -> Result<String, ServiceManagerError> { | ||||||
|  |         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||||
|  |         rt.block_on(async { | ||||||
|  |             let log_path = self.get_log_path(service_name); | ||||||
|  |              | ||||||
|  |             if !log_path.exists() { | ||||||
|  |                 return Ok(String::new()); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             match lines { | ||||||
|  |                 Some(n) => { | ||||||
|  |                     let output = Command::new("tail") | ||||||
|  |                         .args(&["-n", &n.to_string(), &log_path.to_string_lossy()]) | ||||||
|  |                         .output() | ||||||
|  |                         .await?; | ||||||
|  |                     Ok(String::from_utf8_lossy(&output.stdout).to_string()) | ||||||
|  |                 } | ||||||
|  |                 None => { | ||||||
|  |                     let content = tokio::fs::read_to_string(&log_path).await?; | ||||||
|  |                     Ok(content) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn list(&self) -> Result<Vec<String>, ServiceManagerError> { | ||||||
|  |         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||||
|  |         rt.block_on(async { | ||||||
|  |             let list_output = self.run_launchctl(&["list"]).await?; | ||||||
|  |              | ||||||
|  |             let services: Vec<String> = list_output | ||||||
|  |                 .lines() | ||||||
|  |                 .filter_map(|line| { | ||||||
|  |                     if line.contains(&self.service_prefix) { | ||||||
|  |                         // Extract service name from label | ||||||
|  |                         line.split_whitespace() | ||||||
|  |                             .last() | ||||||
|  |                             .and_then(|label| label.strip_prefix(&format!("{}.", self.service_prefix))) | ||||||
|  |                             .map(|s| s.to_string()) | ||||||
|  |                     } else { | ||||||
|  |                         None | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |                 .collect(); | ||||||
|  |  | ||||||
|  |             Ok(services) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||||
|  |         // Stop the service first | ||||||
|  |         let _ = self.stop(service_name); | ||||||
|  |  | ||||||
|  |         // Remove the plist file | ||||||
|  |         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||||
|  |         rt.block_on(async { | ||||||
|  |             let plist_path = self.get_plist_path(service_name); | ||||||
|  |             if plist_path.exists() { | ||||||
|  |                 tokio::fs::remove_file(&plist_path).await?; | ||||||
|  |             } | ||||||
|  |             Ok(()) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										112
									
								
								service_manager/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								service_manager/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | |||||||
|  | use async_trait::async_trait; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use thiserror::Error; | ||||||
|  |  | ||||||
|  | #[derive(Error, Debug)] | ||||||
|  | pub enum ServiceManagerError { | ||||||
|  |     #[error("Service '{0}' not found")] | ||||||
|  |     ServiceNotFound(String), | ||||||
|  |     #[error("Service '{0}' already exists")] | ||||||
|  |     ServiceAlreadyExists(String), | ||||||
|  |     #[error("Failed to start service '{0}': {1}")] | ||||||
|  |     StartFailed(String, String), | ||||||
|  |     #[error("Failed to stop service '{0}': {1}")] | ||||||
|  |     StopFailed(String, String), | ||||||
|  |     #[error("Failed to restart service '{0}': {1}")] | ||||||
|  |     RestartFailed(String, String), | ||||||
|  |     #[error("Failed to get logs for service '{0}': {1}")] | ||||||
|  |     LogsFailed(String, String), | ||||||
|  |     #[error("IO error: {0}")] | ||||||
|  |     IoError(#[from] std::io::Error), | ||||||
|  |     #[error("Service manager error: {0}")] | ||||||
|  |     Other(String), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub struct ServiceConfig { | ||||||
|  |     pub name: String, | ||||||
|  |     pub binary_path: String, | ||||||
|  |     pub args: Vec<String>, | ||||||
|  |     pub working_directory: Option<String>, | ||||||
|  |     pub environment: HashMap<String, String>, | ||||||
|  |     pub auto_restart: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub enum ServiceStatus { | ||||||
|  |     Running, | ||||||
|  |     Stopped, | ||||||
|  |     Failed, | ||||||
|  |     Unknown, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[async_trait] | ||||||
|  | pub trait ServiceManager: Send + Sync { | ||||||
|  |     /// Check if a service exists | ||||||
|  |     fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError>; | ||||||
|  |      | ||||||
|  |     /// Start a service with the given configuration | ||||||
|  |     fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError>; | ||||||
|  |      | ||||||
|  |     /// Start an existing service by name (load existing plist/config) | ||||||
|  |     fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||||
|  |      | ||||||
|  |     /// Start a service and wait for confirmation that it's running or failed | ||||||
|  |     async fn start_and_confirm(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError>; | ||||||
|  |      | ||||||
|  |     /// Start a service and wait for confirmation that it's running or failed | ||||||
|  |     async fn run(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError>; | ||||||
|  |      | ||||||
|  |     /// Start an existing service and wait for confirmation that it's running or failed | ||||||
|  |     async fn start_existing_and_confirm(&self, service_name: &str, timeout_secs: u64) -> Result<(), ServiceManagerError>; | ||||||
|  |      | ||||||
|  |     /// Stop a service by name | ||||||
|  |     fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||||
|  |      | ||||||
|  |     /// Restart a service by name | ||||||
|  |     fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||||
|  |      | ||||||
|  |     /// Get the status of a service | ||||||
|  |     fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError>; | ||||||
|  |      | ||||||
|  |     /// Get logs for a service | ||||||
|  |     fn logs(&self, service_name: &str, lines: Option<usize>) -> Result<String, ServiceManagerError>; | ||||||
|  |      | ||||||
|  |     /// List all managed services | ||||||
|  |     fn list(&self) -> Result<Vec<String>, ServiceManagerError>; | ||||||
|  |      | ||||||
|  |     /// Remove a service configuration (stop if running) | ||||||
|  |     fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Platform-specific implementations | ||||||
|  | #[cfg(target_os = "macos")] | ||||||
|  | mod launchctl; | ||||||
|  | #[cfg(target_os = "macos")] | ||||||
|  | pub use launchctl::LaunchctlServiceManager; | ||||||
|  |  | ||||||
|  | #[cfg(target_os = "linux")] | ||||||
|  | mod systemd; | ||||||
|  | #[cfg(target_os = "linux")] | ||||||
|  | pub use systemd::SystemdServiceManager; | ||||||
|  |  | ||||||
|  | #[cfg(feature = "zinit")] | ||||||
|  | mod zinit; | ||||||
|  | #[cfg(feature = "zinit")] | ||||||
|  | pub use zinit::ZinitServiceManager; | ||||||
|  |  | ||||||
|  | // Factory function to create the appropriate service manager for the platform | ||||||
|  | pub fn create_service_manager() -> Box<dyn ServiceManager> { | ||||||
|  |     #[cfg(target_os = "macos")] | ||||||
|  |     { | ||||||
|  |         Box::new(LaunchctlServiceManager::new()) | ||||||
|  |     } | ||||||
|  |     #[cfg(target_os = "linux")] | ||||||
|  |     { | ||||||
|  |         Box::new(SystemdServiceManager::new()) | ||||||
|  |     } | ||||||
|  |     #[cfg(not(any(target_os = "macos", target_os = "linux")))] | ||||||
|  |     { | ||||||
|  |         compile_error!("Service manager not implemented for this platform") | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								service_manager/src/systemd.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								service_manager/src/systemd.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; | ||||||
|  | use async_trait::async_trait; | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct SystemdServiceManager; | ||||||
|  |  | ||||||
|  | impl SystemdServiceManager { | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         Self | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[async_trait] | ||||||
|  | impl ServiceManager for SystemdServiceManager { | ||||||
|  |     async fn start(&self, _config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||||
|  |         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn stop(&self, _service_name: &str) -> Result<(), ServiceManagerError> { | ||||||
|  |         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn restart(&self, _service_name: &str) -> Result<(), ServiceManagerError> { | ||||||
|  |         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn status(&self, _service_name: &str) -> Result<ServiceStatus, ServiceManagerError> { | ||||||
|  |         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn logs(&self, _service_name: &str, _lines: Option<usize>) -> Result<String, ServiceManagerError> { | ||||||
|  |         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn list(&self) -> Result<Vec<String>, ServiceManagerError> { | ||||||
|  |         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn remove(&self, _service_name: &str) -> Result<(), ServiceManagerError> { | ||||||
|  |         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										122
									
								
								service_manager/src/zinit.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								service_manager/src/zinit.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | |||||||
|  | use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use serde_json::json; | ||||||
|  | use std::sync::Arc; | ||||||
|  | use zinit_client::{get_zinit_client, ServiceStatus as ZinitServiceStatus, ZinitClientWrapper}; | ||||||
|  |  | ||||||
|  | pub struct ZinitServiceManager { | ||||||
|  |     client: Arc<ZinitClientWrapper>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ZinitServiceManager { | ||||||
|  |     pub fn new(socket_path: &str) -> Result<Self, ServiceManagerError> { | ||||||
|  |         // This is a blocking call to get the async client. | ||||||
|  |         // We might want to make this async in the future if the constructor can be async. | ||||||
|  |         let client = tokio::runtime::Runtime::new() | ||||||
|  |             .unwrap() | ||||||
|  |             .block_on(get_zinit_client(socket_path)) | ||||||
|  |             .map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||||
|  |         Ok(ZinitServiceManager { client }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[async_trait] | ||||||
|  | impl ServiceManager for ZinitServiceManager { | ||||||
|  |     fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> { | ||||||
|  |         let status_res = self.status(service_name); | ||||||
|  |         match status_res { | ||||||
|  |             Ok(_) => Ok(true), | ||||||
|  |             Err(ServiceManagerError::ServiceNotFound(_)) => Ok(false), | ||||||
|  |             Err(e) => Err(e), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||||
|  |         let service_config = json!({ | ||||||
|  |             "exec": config.binary_path, | ||||||
|  |             "args": config.args, | ||||||
|  |             "working_directory": config.working_directory, | ||||||
|  |             "env": config.environment, | ||||||
|  |             "restart": config.auto_restart, | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         tokio::runtime::Runtime::new() | ||||||
|  |             .unwrap() | ||||||
|  |             .block_on(self.client.create_service(&config.name, service_config)) | ||||||
|  |             .map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?; | ||||||
|  |  | ||||||
|  |         self.start_existing(&config.name) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||||
|  |         tokio::runtime::Runtime::new() | ||||||
|  |             .unwrap() | ||||||
|  |             .block_on(self.client.start(service_name)) | ||||||
|  |             .map_err(|e| ServiceManagerError::StartFailed(service_name.to_string(), e.to_string())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn start_and_confirm(&self, config: &ServiceConfig, _timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||||
|  |         self.start(config) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn run(&self, config: &ServiceConfig, _timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||||
|  |         self.start(config) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn start_existing_and_confirm(&self, service_name: &str, _timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||||
|  |         self.start_existing(service_name) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||||
|  |         tokio::runtime::Runtime::new() | ||||||
|  |             .unwrap() | ||||||
|  |             .block_on(self.client.stop(service_name)) | ||||||
|  |             .map_err(|e| ServiceManagerError::StopFailed(service_name.to_string(), e.to_string())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||||
|  |         tokio::runtime::Runtime::new() | ||||||
|  |             .unwrap() | ||||||
|  |             .block_on(self.client.restart(service_name)) | ||||||
|  |             .map_err(|e| ServiceManagerError::RestartFailed(service_name.to_string(), e.to_string())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> { | ||||||
|  |         let status: ZinitServiceStatus = tokio::runtime::Runtime::new() | ||||||
|  |             .unwrap() | ||||||
|  |             .block_on(self.client.status(service_name)) | ||||||
|  |             .map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||||
|  |  | ||||||
|  |         let service_status = match status { | ||||||
|  |             ZinitServiceStatus::Running(_) => crate::ServiceStatus::Running, | ||||||
|  |             ZinitServiceStatus::Stopped => crate::ServiceStatus::Stopped, | ||||||
|  |             ZinitServiceStatus::Failed(_) => crate::ServiceStatus::Failed, | ||||||
|  |             ZinitServiceStatus::Waiting(_) => crate::ServiceStatus::Unknown, | ||||||
|  |         }; | ||||||
|  |         Ok(service_status) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn logs(&self, service_name: &str, _lines: Option<usize>) -> Result<String, ServiceManagerError> { | ||||||
|  |         let logs = tokio::runtime::Runtime::new() | ||||||
|  |             .unwrap() | ||||||
|  |             .block_on(self.client.logs(Some(service_name.to_string()))) | ||||||
|  |             .map_err(|e| ServiceManagerError::LogsFailed(service_name.to_string(), e.to_string()))?; | ||||||
|  |         Ok(logs.join("\n")) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn list(&self) -> Result<Vec<String>, ServiceManagerError> { | ||||||
|  |         let services = tokio::runtime::Runtime::new() | ||||||
|  |             .unwrap() | ||||||
|  |             .block_on(self.client.list()) | ||||||
|  |             .map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||||
|  |         Ok(services.keys().cloned().collect()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||||
|  |         let _ = self.stop(service_name); // Best effort to stop before removing | ||||||
|  |         tokio::runtime::Runtime::new() | ||||||
|  |             .unwrap() | ||||||
|  |             .block_on(self.client.delete_service(service_name)) | ||||||
|  |             .map_err(|e| ServiceManagerError::Other(e.to_string())) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -2,7 +2,6 @@ use crate::buildah::{ | |||||||
|     execute_buildah_command, set_thread_local_debug, thread_local_debug, BuildahError, Image, |     execute_buildah_command, set_thread_local_debug, thread_local_debug, BuildahError, Image, | ||||||
| }; | }; | ||||||
| use sal_process::CommandResult; | use sal_process::CommandResult; | ||||||
| use std::collections::HashMap; |  | ||||||
|  |  | ||||||
| /// Builder struct for buildah operations | /// Builder struct for buildah operations | ||||||
| #[derive(Clone)] | #[derive(Clone)] | ||||||
| @@ -249,8 +248,32 @@ impl Builder { | |||||||
|     /// # Returns |     /// # Returns | ||||||
|     /// |     /// | ||||||
|     /// * `Result<CommandResult, BuildahError>` - Command result or error |     /// * `Result<CommandResult, BuildahError>` - Command result or error | ||||||
|     pub fn commit(&self, image_name: &str) -> Result<CommandResult, BuildahError> { |     pub fn commit(&self, image_name: &str, options: Option<Vec<(String, String)>>) -> Result<CommandResult, BuildahError> { | ||||||
|         if let Some(container_id) = &self.container_id { |         if let Some(container_id) = &self.container_id { | ||||||
|  |             let mut args_owned: Vec<String> = Vec::new(); | ||||||
|  |             args_owned.push("commit".to_string()); | ||||||
|  |  | ||||||
|  |             // Process options | ||||||
|  |             if let Some(options_vec) = options { | ||||||
|  |                 for (key, value) in options_vec.iter() { | ||||||
|  |                     let option_name = if key.len() == 1 { | ||||||
|  |                         format!("-{}", key) | ||||||
|  |                     } else { | ||||||
|  |                         format!("--{}", key) | ||||||
|  |                     }; | ||||||
|  |                     args_owned.push(option_name); | ||||||
|  |                     if !value.is_empty() { | ||||||
|  |                         args_owned.push(value.clone()); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             args_owned.push(container_id.clone()); | ||||||
|  |             args_owned.push(image_name.to_string()); | ||||||
|  |  | ||||||
|  |             // Convert Vec<String> to Vec<&str> for execute_buildah_command | ||||||
|  |             let args: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect(); | ||||||
|  |  | ||||||
|             // Save the current debug flag |             // Save the current debug flag | ||||||
|             let previous_debug = thread_local_debug(); |             let previous_debug = thread_local_debug(); | ||||||
|  |  | ||||||
| @@ -258,7 +281,7 @@ impl Builder { | |||||||
|             set_thread_local_debug(self.debug); |             set_thread_local_debug(self.debug); | ||||||
|  |  | ||||||
|             // Execute the command |             // Execute the command | ||||||
|             let result = execute_buildah_command(&["commit", container_id, image_name]); |             let result = execute_buildah_command(&args); | ||||||
|  |  | ||||||
|             // Restore the previous debug flag |             // Restore the previous debug flag | ||||||
|             set_thread_local_debug(previous_debug); |             set_thread_local_debug(previous_debug); | ||||||
| @@ -336,17 +359,23 @@ impl Builder { | |||||||
|     /// # Returns |     /// # Returns | ||||||
|     /// |     /// | ||||||
|     /// * `Result<CommandResult, BuildahError>` - Command result or error |     /// * `Result<CommandResult, BuildahError>` - Command result or error | ||||||
|     pub fn config(&self, options: HashMap<String, String>) -> Result<CommandResult, BuildahError> { |     pub fn config(&self, options: Vec<(String, String)>) -> Result<CommandResult, BuildahError> { | ||||||
|         if let Some(container_id) = &self.container_id { |         if let Some(container_id) = &self.container_id { | ||||||
|             let mut args_owned: Vec<String> = Vec::new(); |             let mut args_owned: Vec<String> = Vec::new(); | ||||||
|             args_owned.push("config".to_string()); |             args_owned.push("config".to_string()); | ||||||
|  |  | ||||||
|             // Process options map |             // Process options | ||||||
|             for (key, value) in options.iter() { |             for (key, value) in options.iter() { | ||||||
|                 let option_name = format!("--{}", key); |                 let option_name = if key.len() == 1 { | ||||||
|  |                     format!("-{}", key) | ||||||
|  |                 } else { | ||||||
|  |                     format!("--{}", key) | ||||||
|  |                 }; | ||||||
|                 args_owned.push(option_name); |                 args_owned.push(option_name); | ||||||
|  |                 if !value.is_empty() { | ||||||
|                     args_owned.push(value.clone()); |                     args_owned.push(value.clone()); | ||||||
|                 } |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|             args_owned.push(container_id.clone()); |             args_owned.push(container_id.clone()); | ||||||
|  |  | ||||||
| @@ -380,8 +409,19 @@ impl Builder { | |||||||
|     /// # Returns |     /// # Returns | ||||||
|     /// |     /// | ||||||
|     /// * `Result<CommandResult, BuildahError>` - Command result or error |     /// * `Result<CommandResult, BuildahError>` - Command result or error | ||||||
|     pub fn set_entrypoint(&self, entrypoint: &str) -> Result<CommandResult, BuildahError> { |     pub fn set_entrypoint(&self, entrypoint: Vec<String>) -> Result<CommandResult, BuildahError> { | ||||||
|         if let Some(container_id) = &self.container_id { |         if let Some(container_id) = &self.container_id { | ||||||
|  |             // Serialize the entrypoint vector to a JSON string | ||||||
|  |             let entrypoint_json = match serde_json::to_string(&entrypoint) { | ||||||
|  |                 Ok(json) => json, | ||||||
|  |                 Err(e) => { | ||||||
|  |                     return Err(BuildahError::JsonParseError(format!( | ||||||
|  |                         "Failed to serialize entrypoint to JSON: {}", | ||||||
|  |                         e | ||||||
|  |                     ))); | ||||||
|  |                 } | ||||||
|  |             }; | ||||||
|  |  | ||||||
|             // Save the current debug flag |             // Save the current debug flag | ||||||
|             let previous_debug = thread_local_debug(); |             let previous_debug = thread_local_debug(); | ||||||
|  |  | ||||||
| @@ -390,7 +430,7 @@ impl Builder { | |||||||
|  |  | ||||||
|             // Execute the command |             // Execute the command | ||||||
|             let result = |             let result = | ||||||
|                 execute_buildah_command(&["config", "--entrypoint", entrypoint, container_id]); |                 execute_buildah_command(&["config", "--entrypoint", &entrypoint_json, container_id]); | ||||||
|  |  | ||||||
|             // Restore the previous debug flag |             // Restore the previous debug flag | ||||||
|             set_thread_local_debug(previous_debug); |             set_thread_local_debug(previous_debug); | ||||||
| @@ -410,8 +450,19 @@ impl Builder { | |||||||
|     /// # Returns |     /// # Returns | ||||||
|     /// |     /// | ||||||
|     /// * `Result<CommandResult, BuildahError>` - Command result or error |     /// * `Result<CommandResult, BuildahError>` - Command result or error | ||||||
|     pub fn set_cmd(&self, cmd: &str) -> Result<CommandResult, BuildahError> { |     pub fn set_cmd(&self, cmd: Vec<String>) -> Result<CommandResult, BuildahError> { | ||||||
|         if let Some(container_id) = &self.container_id { |         if let Some(container_id) = &self.container_id { | ||||||
|  |             // Serialize the cmd vector to a JSON string | ||||||
|  |             let cmd_json = match serde_json::to_string(&cmd) { | ||||||
|  |                 Ok(json) => json, | ||||||
|  |                 Err(e) => { | ||||||
|  |                     return Err(BuildahError::JsonParseError(format!( | ||||||
|  |                         "Failed to serialize cmd to JSON: {}", | ||||||
|  |                         e | ||||||
|  |                     ))); | ||||||
|  |                 } | ||||||
|  |             }; | ||||||
|  |  | ||||||
|             // Save the current debug flag |             // Save the current debug flag | ||||||
|             let previous_debug = thread_local_debug(); |             let previous_debug = thread_local_debug(); | ||||||
|  |  | ||||||
| @@ -419,7 +470,7 @@ impl Builder { | |||||||
|             set_thread_local_debug(self.debug); |             set_thread_local_debug(self.debug); | ||||||
|  |  | ||||||
|             // Execute the command |             // Execute the command | ||||||
|             let result = execute_buildah_command(&["config", "--cmd", cmd, container_id]); |             let result = execute_buildah_command(&["config", "--cmd", &cmd_json, container_id]); | ||||||
|  |  | ||||||
|             // Restore the previous debug flag |             // Restore the previous debug flag | ||||||
|             set_thread_local_debug(previous_debug); |             set_thread_local_debug(previous_debug); | ||||||
|   | |||||||
| @@ -55,34 +55,44 @@ impl ContentOperations { | |||||||
|     /// |     /// | ||||||
|     /// * `Result<String, BuildahError>` - File content or error |     /// * `Result<String, BuildahError>` - File content or error | ||||||
|     pub fn read_content(container_id: &str, source_path: &str) -> Result<String, BuildahError> { |     pub fn read_content(container_id: &str, source_path: &str) -> Result<String, BuildahError> { | ||||||
|         // Create a temporary file |         // Create a temporary file to store the content from the container. | ||||||
|         let temp_file = NamedTempFile::new() |         let temp_file = NamedTempFile::new() | ||||||
|             .map_err(|e| BuildahError::Other(format!("Failed to create temporary file: {}", e)))?; |             .map_err(|e| BuildahError::Other(format!("Failed to create temporary file: {}", e)))?; | ||||||
|  |  | ||||||
|         let temp_path = temp_file.path().to_string_lossy().to_string(); |         let temp_path = temp_file.path().to_string_lossy().to_string(); | ||||||
|  |  | ||||||
|         // Copy the file from the container to the temporary file |         // In rootless mode, `buildah mount` must run inside `buildah unshare`. | ||||||
|         // Use mount to access the container's filesystem |         // We create a script to mount, copy the file, and unmount, all within the unshare session. | ||||||
|         let mount_result = execute_buildah_command(&["mount", container_id])?; |         let script = format!( | ||||||
|         let mount_point = mount_result.stdout.trim(); |             r#" | ||||||
|  |                 set -e | ||||||
|  |                 mount_point=$(buildah mount '{container_id}') | ||||||
|  |                 if [ -z "$mount_point" ]; then | ||||||
|  |                     echo "Error: Failed to mount container '{container_id}'." >&2 | ||||||
|  |                     exit 1 | ||||||
|  |                 fi | ||||||
|  |                 trap 'buildah umount '{container_id}'' EXIT | ||||||
|  |                 cp "${{mount_point}}{source_path}" '{temp_path}' | ||||||
|  |             "#, | ||||||
|  |             container_id = container_id, | ||||||
|  |             source_path = source_path, | ||||||
|  |             temp_path = &temp_path | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         // Construct the full path to the file in the container |         let result = execute_buildah_command(&["unshare", "sh", "-c", &script])?; | ||||||
|         let full_source_path = format!("{}{}", mount_point, source_path); |  | ||||||
|  |  | ||||||
|         // Copy the file from the mounted container to the temporary file |         if !result.success { | ||||||
|         execute_buildah_command(&["copy", container_id, &full_source_path, &temp_path])?; |             return Err(BuildahError::Other(format!( | ||||||
|  |                 "Failed to execute read_content script in unshare session: {}", | ||||||
|         // Unmount the container |                 result.stderr | ||||||
|         execute_buildah_command(&["umount", container_id])?; |             ))); | ||||||
|  |         } | ||||||
|         // Read the content from the temporary file |  | ||||||
|         let mut file = File::open(temp_file.path()) |  | ||||||
|             .map_err(|e| BuildahError::Other(format!("Failed to open temporary file: {}", e)))?; |  | ||||||
|  |  | ||||||
|  |         // The script has copied the file content to our temporary file. | ||||||
|  |         // Now, we read it. | ||||||
|         let mut content = String::new(); |         let mut content = String::new(); | ||||||
|         file.read_to_string(&mut content).map_err(|e| { |         File::open(&temp_path) | ||||||
|             BuildahError::Other(format!("Failed to read from temporary file: {}", e)) |             .and_then(|mut f| f.read_to_string(&mut content)) | ||||||
|         })?; |             .map_err(|e| BuildahError::Other(format!("Failed to read from temporary file: {}", e)))?; | ||||||
|  |  | ||||||
|         Ok(content) |         Ok(content) | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -52,6 +52,20 @@ impl Container { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// Add an image | ||||||
|  |     ///  | ||||||
|  |     /// # Arguments | ||||||
|  |     ///  | ||||||
|  |     /// * `image` - Image to create the container from | ||||||
|  |     ///  | ||||||
|  |     /// # Returns | ||||||
|  |     ///  | ||||||
|  |     /// * `Self` - The container instance for method chaining | ||||||
|  |     pub fn with_image(mut self, image: &str) -> Self { | ||||||
|  |         self.image = Some(image.to_string()); | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /// Add a port mapping |     /// Add a port mapping | ||||||
|     /// |     /// | ||||||
|     /// # Arguments |     /// # Arguments | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ pub struct Container { | |||||||
| } | } | ||||||
|  |  | ||||||
| /// Health check configuration for a container | /// Health check configuration for a container | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone, PartialEq)] | ||||||
| pub struct HealthCheck { | pub struct HealthCheck { | ||||||
|     /// Command to run for health check |     /// Command to run for health check | ||||||
|     pub cmd: String, |     pub cmd: String, | ||||||
|   | |||||||
| @@ -3,9 +3,8 @@ | |||||||
| //! This module provides Rhai wrappers for the functions in the Buildah module. | //! This module provides Rhai wrappers for the functions in the Buildah module. | ||||||
|  |  | ||||||
| use crate::buildah::{BuildahError, Builder, ContentOperations, Image}; | use crate::buildah::{BuildahError, Builder, ContentOperations, Image}; | ||||||
| use rhai::{Array, Dynamic, Engine, EvalAltResult, Map}; | use rhai::{Array, Dynamic, Engine, EvalAltResult}; | ||||||
| use sal_process::CommandResult; | use sal_process::CommandResult; | ||||||
| use std::collections::HashMap; |  | ||||||
|  |  | ||||||
| /// Register Buildah module functions with the Rhai engine | /// Register Buildah module functions with the Rhai engine | ||||||
| /// | /// | ||||||
| @@ -45,7 +44,6 @@ pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult> | |||||||
|     engine.register_fn("image_push", builder_image_push); |     engine.register_fn("image_push", builder_image_push); | ||||||
|     engine.register_fn("image_tag", builder_image_tag); |     engine.register_fn("image_tag", builder_image_tag); | ||||||
|     engine.register_fn("build", builder_build); |     engine.register_fn("build", builder_build); | ||||||
|     engine.register_fn("read_content", builder_read_content); |  | ||||||
|  |  | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
| @@ -89,29 +87,51 @@ fn register_bah_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Helper functions for error conversion | // Helper functions for error conversion | ||||||
| fn bah_error_to_rhai_error<T>(result: Result<T, BuildahError>) -> Result<T, Box<EvalAltResult>> { | impl From<BuildahError> for Box<EvalAltResult> { | ||||||
|     result.map_err(|e| { |     fn from(err: BuildahError) -> Self { | ||||||
|         Box::new(EvalAltResult::ErrorRuntime( |         Box::new(EvalAltResult::ErrorRuntime( | ||||||
|             format!("Buildah error: {}", e).into(), |             format!("Buildah error: {}", err).into(), | ||||||
|             rhai::Position::NONE, |             rhai::Position::NONE, | ||||||
|         )) |         )) | ||||||
|     }) |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| // Helper function to convert Rhai Map to Rust HashMap | // Helper function to convert Rhai Array of pairs to a Vec of tuples | ||||||
| fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<EvalAltResult>> { | fn convert_array_to_vec( | ||||||
|     let mut config_options = HashMap::<String, String>::new(); |     options: Array, | ||||||
|  | ) -> Result<Vec<(String, String)>, Box<EvalAltResult>> { | ||||||
|  |     let mut config_options: Vec<(String, String)> = Vec::new(); | ||||||
|  |  | ||||||
|     for (key, value) in options.iter() { |     for option_pair_dynamic in options { | ||||||
|         if let Ok(value_str) = value.clone().into_string() { |         let pair = option_pair_dynamic.into_array().map_err(|_e| { | ||||||
|             // Convert SmartString to String |             Box::new(EvalAltResult::ErrorRuntime( | ||||||
|             config_options.insert(key.to_string(), value_str); |                 "Each option must be an array of [key, value]".into(), | ||||||
|         } else { |                 rhai::Position::NONE, | ||||||
|  |             )) | ||||||
|  |         })?; | ||||||
|  |  | ||||||
|  |         if pair.len() != 2 { | ||||||
|             return Err(Box::new(EvalAltResult::ErrorRuntime( |             return Err(Box::new(EvalAltResult::ErrorRuntime( | ||||||
|                 format!("Option '{}' must be a string", key).into(), |                 "Each option must be an array of [key, value] with 2 elements".into(), | ||||||
|                 rhai::Position::NONE, |                 rhai::Position::NONE, | ||||||
|             ))); |             ))); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         let key = pair[0].clone().into_string().map_err(|_e| { | ||||||
|  |             Box::new(EvalAltResult::ErrorRuntime( | ||||||
|  |                 "Option key must be a string".into(), | ||||||
|  |                 rhai::Position::NONE, | ||||||
|  |             )) | ||||||
|  |         })?; | ||||||
|  |  | ||||||
|  |         let value = pair[1].clone().into_string().map_err(|_e| { | ||||||
|  |             Box::new(EvalAltResult::ErrorRuntime( | ||||||
|  |                 "Option value must be a string".into(), | ||||||
|  |                 rhai::Position::NONE, | ||||||
|  |             )) | ||||||
|  |         })?; | ||||||
|  |  | ||||||
|  |         config_options.push((key, value)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     Ok(config_options) |     Ok(config_options) | ||||||
| @@ -119,7 +139,7 @@ fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<E | |||||||
|  |  | ||||||
| /// Create a new Builder | /// Create a new Builder | ||||||
| pub fn bah_new(name: &str, image: &str) -> Result<Builder, Box<EvalAltResult>> { | pub fn bah_new(name: &str, image: &str) -> Result<Builder, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(Builder::new(name, image)) |     Builder::new(name, image).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Builder instance methods | // Builder instance methods | ||||||
| @@ -127,7 +147,7 @@ pub fn builder_run( | |||||||
|     builder: &mut Builder, |     builder: &mut Builder, | ||||||
|     command: &str, |     command: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(builder.run(command)) |     builder.run(command).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn builder_run_with_isolation( | pub fn builder_run_with_isolation( | ||||||
| @@ -135,7 +155,9 @@ pub fn builder_run_with_isolation( | |||||||
|     command: &str, |     command: &str, | ||||||
|     isolation: &str, |     isolation: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(builder.run_with_isolation(command, isolation)) |     builder | ||||||
|  |         .run_with_isolation(command, isolation) | ||||||
|  |         .map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn builder_copy( | pub fn builder_copy( | ||||||
| @@ -143,7 +165,7 @@ pub fn builder_copy( | |||||||
|     source: &str, |     source: &str, | ||||||
|     dest: &str, |     dest: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(builder.copy(source, dest)) |     builder.copy(source, dest).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn builder_add( | pub fn builder_add( | ||||||
| @@ -151,43 +173,54 @@ pub fn builder_add( | |||||||
|     source: &str, |     source: &str, | ||||||
|     dest: &str, |     dest: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(builder.add(source, dest)) |     builder.add(source, dest).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn builder_commit( | pub fn builder_commit( | ||||||
|     builder: &mut Builder, |     builder: &mut Builder, | ||||||
|     image_name: &str, |     image_name: &str, | ||||||
|  |     options: Array, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(builder.commit(image_name)) |     let commit_options = convert_array_to_vec(options)?; | ||||||
|  |     builder | ||||||
|  |         .commit(image_name, Some(commit_options)) | ||||||
|  |         .map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn builder_remove(builder: &mut Builder) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn builder_remove(builder: &mut Builder) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(builder.remove()) |     builder.remove().map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn builder_config( | pub fn builder_config( | ||||||
|     builder: &mut Builder, |     builder: &mut Builder, | ||||||
|     options: Map, |     options: Array, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     // Convert Rhai Map to Rust HashMap |     let config_options = convert_array_to_vec(options)?; | ||||||
|     let config_options = convert_map_to_hashmap(options)?; |     builder.config(config_options).map_err(Into::into) | ||||||
|     bah_error_to_rhai_error(builder.config(config_options)) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Set the entrypoint for the container | /// Set the entrypoint for the container | ||||||
| pub fn builder_set_entrypoint( | pub fn builder_set_entrypoint( | ||||||
|     builder: &mut Builder, |     builder: &mut Builder, | ||||||
|     entrypoint: &str, |     entrypoint: Array, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(builder.set_entrypoint(entrypoint)) |     let entrypoint_vec: Vec<String> = entrypoint | ||||||
|  |         .into_iter() | ||||||
|  |         .map(|item| item.into_string().unwrap_or_default()) | ||||||
|  |         .collect(); | ||||||
|  |     builder.set_entrypoint(entrypoint_vec).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Set the default command for the container | /// Set the default command for the container | ||||||
| pub fn builder_set_cmd( | pub fn builder_set_cmd( | ||||||
|     builder: &mut Builder, |     builder: &mut Builder, | ||||||
|     cmd: &str, |     cmd: Array, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(builder.set_cmd(cmd)) |     let cmd_vec: Vec<String> = cmd | ||||||
|  |         .into_iter() | ||||||
|  |         .map(|item| item.into_string().unwrap_or_default()) | ||||||
|  |         .collect(); | ||||||
|  |     builder.set_cmd(cmd_vec).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Write content to a file in the container | /// Write content to a file in the container | ||||||
| @@ -197,11 +230,7 @@ pub fn builder_write_content( | |||||||
|     dest_path: &str, |     dest_path: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     if let Some(container_id) = builder.container_id() { |     if let Some(container_id) = builder.container_id() { | ||||||
|         bah_error_to_rhai_error(ContentOperations::write_content( |         ContentOperations::write_content(container_id, content, dest_path).map_err(Into::into) | ||||||
|             container_id, |  | ||||||
|             content, |  | ||||||
|             dest_path, |  | ||||||
|         )) |  | ||||||
|     } else { |     } else { | ||||||
|         Err(Box::new(EvalAltResult::ErrorRuntime( |         Err(Box::new(EvalAltResult::ErrorRuntime( | ||||||
|             "No container ID available".into(), |             "No container ID available".into(), | ||||||
| @@ -216,7 +245,7 @@ pub fn builder_read_content( | |||||||
|     source_path: &str, |     source_path: &str, | ||||||
| ) -> Result<String, Box<EvalAltResult>> { | ) -> Result<String, Box<EvalAltResult>> { | ||||||
|     if let Some(container_id) = builder.container_id() { |     if let Some(container_id) = builder.container_id() { | ||||||
|         bah_error_to_rhai_error(ContentOperations::read_content(container_id, source_path)) |         ContentOperations::read_content(container_id, source_path).map_err(Into::into) | ||||||
|     } else { |     } else { | ||||||
|         Err(Box::new(EvalAltResult::ErrorRuntime( |         Err(Box::new(EvalAltResult::ErrorRuntime( | ||||||
|             "No container ID available".into(), |             "No container ID available".into(), | ||||||
| @@ -227,7 +256,7 @@ pub fn builder_read_content( | |||||||
|  |  | ||||||
| // Builder static methods | // Builder static methods | ||||||
| pub fn builder_images(_builder: &mut Builder) -> Result<Array, Box<EvalAltResult>> { | pub fn builder_images(_builder: &mut Builder) -> Result<Array, Box<EvalAltResult>> { | ||||||
|     let images = bah_error_to_rhai_error(Builder::images())?; |     let images = Builder::images()?; | ||||||
|  |  | ||||||
|     // Convert Vec<Image> to Rhai Array |     // Convert Vec<Image> to Rhai Array | ||||||
|     let mut array = Array::new(); |     let mut array = Array::new(); | ||||||
| @@ -242,7 +271,7 @@ pub fn builder_image_remove( | |||||||
|     _builder: &mut Builder, |     _builder: &mut Builder, | ||||||
|     image: &str, |     image: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(Builder::image_remove(image)) |     Builder::image_remove(image).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn builder_image_pull( | pub fn builder_image_pull( | ||||||
| @@ -250,7 +279,7 @@ pub fn builder_image_pull( | |||||||
|     image: &str, |     image: &str, | ||||||
|     tls_verify: bool, |     tls_verify: bool, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(Builder::image_pull(image, tls_verify)) |     Builder::image_pull(image, tls_verify).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn builder_image_push( | pub fn builder_image_push( | ||||||
| @@ -259,7 +288,7 @@ pub fn builder_image_push( | |||||||
|     destination: &str, |     destination: &str, | ||||||
|     tls_verify: bool, |     tls_verify: bool, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(Builder::image_push(image, destination, tls_verify)) |     Builder::image_push(image, destination, tls_verify).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn builder_image_tag( | pub fn builder_image_tag( | ||||||
| @@ -267,7 +296,7 @@ pub fn builder_image_tag( | |||||||
|     image: &str, |     image: &str, | ||||||
|     new_name: &str, |     new_name: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(Builder::image_tag(image, new_name)) |     Builder::image_tag(image, new_name).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Getter functions for Builder properties | // Getter functions for Builder properties | ||||||
| @@ -298,7 +327,7 @@ pub fn set_builder_debug(builder: &mut Builder, debug: bool) { | |||||||
|  |  | ||||||
| // Reset function for Builder | // Reset function for Builder | ||||||
| pub fn builder_reset(builder: &mut Builder) -> Result<(), Box<EvalAltResult>> { | pub fn builder_reset(builder: &mut Builder) -> Result<(), Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(builder.reset()) |     builder.reset().map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Build function for Builder | // Build function for Builder | ||||||
| @@ -309,10 +338,5 @@ pub fn builder_build( | |||||||
|     file: &str, |     file: &str, | ||||||
|     isolation: &str, |     isolation: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     bah_error_to_rhai_error(Builder::build( |     Builder::build(Some(tag), context_dir, file, Some(isolation)).map_err(Into::into) | ||||||
|         Some(tag), |  | ||||||
|         context_dir, |  | ||||||
|         file, |  | ||||||
|         Some(isolation), |  | ||||||
|     )) |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,38 +2,37 @@ | |||||||
| //! | //! | ||||||
| //! This module provides Rhai wrappers for the functions in the Nerdctl module. | //! This module provides Rhai wrappers for the functions in the Nerdctl module. | ||||||
|  |  | ||||||
| use crate::nerdctl::{self, Container, Image, NerdctlError}; | use crate::nerdctl::{self, Container, HealthCheck, Image, NerdctlError}; | ||||||
| use rhai::{Array, Dynamic, Engine, EvalAltResult, Map}; | use rhai::{Array, Dynamic, Engine, EvalAltResult, Map}; | ||||||
| use sal_process::CommandResult; | use sal_process::CommandResult; | ||||||
|  |  | ||||||
| // Helper functions for error conversion with improved context | // Helper functions for error conversion with improved context | ||||||
| fn nerdctl_error_to_rhai_error<T>( | impl From<NerdctlError> for Box<EvalAltResult> { | ||||||
|     result: Result<T, NerdctlError>, |     fn from(err: NerdctlError) -> Self { | ||||||
| ) -> Result<T, Box<EvalAltResult>> { |         let error_message = match err { | ||||||
|     result.map_err(|e| { |             NerdctlError::CommandExecutionFailed(io_err) => format!( | ||||||
|         // Create a more detailed error message based on the error type |                 "Failed to execute nerdctl command: {}. This may indicate nerdctl is not installed or not in PATH.", | ||||||
|         let error_message = match &e { |                 io_err | ||||||
|             NerdctlError::CommandExecutionFailed(io_err) => { |             ), | ||||||
|                 format!("Failed to execute nerdctl command: {}. This may indicate nerdctl is not installed or not in PATH.", io_err) |             NerdctlError::CommandFailed(msg) => format!( | ||||||
|             }, |                 "Nerdctl command failed: {}. Check container status and logs for more details.", | ||||||
|             NerdctlError::CommandFailed(msg) => { |                 msg | ||||||
|                 format!("Nerdctl command failed: {}. Check container status and logs for more details.", msg) |             ), | ||||||
|             }, |             NerdctlError::JsonParseError(msg) => format!( | ||||||
|             NerdctlError::JsonParseError(msg) => { |                 "Failed to parse nerdctl JSON output: {}. This may indicate an incompatible nerdctl version.", | ||||||
|                 format!("Failed to parse nerdctl JSON output: {}. This may indicate an incompatible nerdctl version.", msg) |                 msg | ||||||
|             }, |             ), | ||||||
|             NerdctlError::ConversionError(msg) => { |             NerdctlError::ConversionError(msg) => format!( | ||||||
|                 format!("Data conversion error: {}. This may indicate unexpected output format from nerdctl.", msg) |                 "Data conversion error: {}. This may indicate unexpected output format from nerdctl.", | ||||||
|             }, |                 msg | ||||||
|             NerdctlError::Other(msg) => { |             ), | ||||||
|                 format!("Nerdctl error: {}. This is an unexpected error.", msg) |             NerdctlError::Other(msg) => format!("Nerdctl error: {}. This is an unexpected error.", msg), | ||||||
|             }, |  | ||||||
|         }; |         }; | ||||||
|         Box::new(EvalAltResult::ErrorRuntime( |         Box::new(EvalAltResult::ErrorRuntime( | ||||||
|             error_message.into(), |             error_message.into(), | ||||||
|             rhai::Position::NONE |             rhai::Position::NONE, | ||||||
|         )) |         )) | ||||||
|     }) |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| // | // | ||||||
| @@ -42,12 +41,7 @@ fn nerdctl_error_to_rhai_error<T>( | |||||||
|  |  | ||||||
| /// Create a new Container | /// Create a new Container | ||||||
| pub fn container_new(name: &str) -> Result<Container, Box<EvalAltResult>> { | pub fn container_new(name: &str) -> Result<Container, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(Container::new(name)) |     Container::new(name).map_err(Into::into) | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Create a Container from an image |  | ||||||
| pub fn container_from_image(name: &str, image: &str) -> Result<Container, Box<EvalAltResult>> { |  | ||||||
|     nerdctl_error_to_rhai_error(Container::from_image(name, image)) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Reset the container configuration to defaults while keeping the name and image | /// Reset the container configuration to defaults while keeping the name and image | ||||||
| @@ -55,127 +49,10 @@ pub fn container_reset(container: Container) -> Container { | |||||||
|     container.reset() |     container.reset() | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Add a port mapping to a Container | pub fn health_check_new(cmd: &str) -> Result<HealthCheck, Box<EvalAltResult>> { | ||||||
| pub fn container_with_port(container: Container, port: &str) -> Container { |     let health_check = HealthCheck::new(cmd); | ||||||
|     container.with_port(port) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Add a volume mount to a Container |     Ok(health_check) | ||||||
| pub fn container_with_volume(container: Container, volume: &str) -> Container { |  | ||||||
|     container.with_volume(volume) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Add an environment variable to a Container |  | ||||||
| pub fn container_with_env(container: Container, key: &str, value: &str) -> Container { |  | ||||||
|     container.with_env(key, value) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set the network for a Container |  | ||||||
| pub fn container_with_network(container: Container, network: &str) -> Container { |  | ||||||
|     container.with_network(network) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Add a network alias to a Container |  | ||||||
| pub fn container_with_network_alias(container: Container, alias: &str) -> Container { |  | ||||||
|     container.with_network_alias(alias) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set CPU limit for a Container |  | ||||||
| pub fn container_with_cpu_limit(container: Container, cpus: &str) -> Container { |  | ||||||
|     container.with_cpu_limit(cpus) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set memory limit for a Container |  | ||||||
| pub fn container_with_memory_limit(container: Container, memory: &str) -> Container { |  | ||||||
|     container.with_memory_limit(memory) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set restart policy for a Container |  | ||||||
| pub fn container_with_restart_policy(container: Container, policy: &str) -> Container { |  | ||||||
|     container.with_restart_policy(policy) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set health check for a Container |  | ||||||
| pub fn container_with_health_check(container: Container, cmd: &str) -> Container { |  | ||||||
|     container.with_health_check(cmd) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Add multiple port mappings to a Container |  | ||||||
| pub fn container_with_ports(mut container: Container, ports: Array) -> Container { |  | ||||||
|     for port in ports.iter() { |  | ||||||
|         if port.is_string() { |  | ||||||
|             let port_str = port.clone().cast::<String>(); |  | ||||||
|             container = container.with_port(&port_str); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     container |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Add multiple volume mounts to a Container |  | ||||||
| pub fn container_with_volumes(mut container: Container, volumes: Array) -> Container { |  | ||||||
|     for volume in volumes.iter() { |  | ||||||
|         if volume.is_string() { |  | ||||||
|             let volume_str = volume.clone().cast::<String>(); |  | ||||||
|             container = container.with_volume(&volume_str); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     container |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Add multiple environment variables to a Container |  | ||||||
| pub fn container_with_envs(mut container: Container, env_map: Map) -> Container { |  | ||||||
|     for (key, value) in env_map.iter() { |  | ||||||
|         if value.is_string() { |  | ||||||
|             let value_str = value.clone().cast::<String>(); |  | ||||||
|             container = container.with_env(&key, &value_str); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     container |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Add multiple network aliases to a Container |  | ||||||
| pub fn container_with_network_aliases(mut container: Container, aliases: Array) -> Container { |  | ||||||
|     for alias in aliases.iter() { |  | ||||||
|         if alias.is_string() { |  | ||||||
|             let alias_str = alias.clone().cast::<String>(); |  | ||||||
|             container = container.with_network_alias(&alias_str); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     container |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set memory swap limit for a Container |  | ||||||
| pub fn container_with_memory_swap_limit(container: Container, memory_swap: &str) -> Container { |  | ||||||
|     container.with_memory_swap_limit(memory_swap) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set CPU shares for a Container |  | ||||||
| pub fn container_with_cpu_shares(container: Container, shares: &str) -> Container { |  | ||||||
|     container.with_cpu_shares(shares) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set health check with options for a Container |  | ||||||
| pub fn container_with_health_check_options( |  | ||||||
|     container: Container, |  | ||||||
|     cmd: &str, |  | ||||||
|     interval: Option<&str>, |  | ||||||
|     timeout: Option<&str>, |  | ||||||
|     retries: Option<i64>, |  | ||||||
|     start_period: Option<&str>, |  | ||||||
| ) -> Container { |  | ||||||
|     // Convert i64 to u32 for retries |  | ||||||
|     let retries_u32 = retries.map(|r| r as u32); |  | ||||||
|     container.with_health_check_options(cmd, interval, timeout, retries_u32, start_period) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set snapshotter for a Container |  | ||||||
| pub fn container_with_snapshotter(container: Container, snapshotter: &str) -> Container { |  | ||||||
|     container.with_snapshotter(snapshotter) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Set detach mode for a Container |  | ||||||
| pub fn container_with_detach(container: Container, detach: bool) -> Container { |  | ||||||
|     container.with_detach(detach) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Build and run the Container | /// Build and run the Container | ||||||
| @@ -242,7 +119,7 @@ pub fn container_build(container: Container) -> Result<Container, Box<EvalAltRes | |||||||
|                 _ => err, |                 _ => err, | ||||||
|             }; |             }; | ||||||
|  |  | ||||||
|             nerdctl_error_to_rhai_error(Err(enhanced_error)) |             Err(enhanced_error.into()) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -299,19 +176,19 @@ pub fn container_start(container: &mut Container) -> Result<CommandResult, Box<E | |||||||
|                 _ => err, |                 _ => err, | ||||||
|             }; |             }; | ||||||
|  |  | ||||||
|             nerdctl_error_to_rhai_error(Err(enhanced_error)) |             Err(enhanced_error.into()) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Stop the Container | /// Stop the Container | ||||||
| pub fn container_stop(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn container_stop(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(container.stop()) |     container.stop().map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Remove the Container | /// Remove the Container | ||||||
| pub fn container_remove(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn container_remove(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(container.remove()) |     container.remove().map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Execute a command in the Container | /// Execute a command in the Container | ||||||
| @@ -319,7 +196,7 @@ pub fn container_exec( | |||||||
|     container: &mut Container, |     container: &mut Container, | ||||||
|     command: &str, |     command: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(container.exec(command)) |     container.exec(command).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Get container logs | /// Get container logs | ||||||
| @@ -343,7 +220,7 @@ pub fn container_logs(container: &mut Container) -> Result<CommandResult, Box<Ev | |||||||
|                 container_name, container_id, err |                 container_name, container_id, err | ||||||
|             )); |             )); | ||||||
|  |  | ||||||
|             nerdctl_error_to_rhai_error(Err(enhanced_error)) |             Err(enhanced_error.into()) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -354,7 +231,7 @@ pub fn container_copy( | |||||||
|     source: &str, |     source: &str, | ||||||
|     dest: &str, |     dest: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(container.copy(source, dest)) |     container.copy(source, dest).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Create a new Map with default run options | /// Create a new Map with default run options | ||||||
| @@ -375,12 +252,12 @@ pub fn new_run_options() -> Map { | |||||||
| /// | /// | ||||||
| /// Run a container from an image. | /// Run a container from an image. | ||||||
| pub fn nerdctl_run(image: &str) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_run(image: &str) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::run(image, None, true, None, None)) |     nerdctl::run(image, None, true, None, None).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Run a container with a name | /// Run a container with a name | ||||||
| pub fn nerdctl_run_with_name(image: &str, name: &str) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_run_with_name(image: &str, name: &str) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::run(image, Some(name), true, None, None)) |     nerdctl::run(image, Some(name), true, None, None).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Run a container with a port mapping | /// Run a container with a port mapping | ||||||
| @@ -390,49 +267,49 @@ pub fn nerdctl_run_with_port( | |||||||
|     port: &str, |     port: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     let ports = vec![port]; |     let ports = vec![port]; | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::run(image, Some(name), true, Some(&ports), None)) |     nerdctl::run(image, Some(name), true, Some(&ports), None).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::exec | /// Wrapper for nerdctl::exec | ||||||
| /// | /// | ||||||
| /// Execute a command in a container. | /// Execute a command in a container. | ||||||
| pub fn nerdctl_exec(container: &str, command: &str) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_exec(container: &str, command: &str) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::exec(container, command)) |     nerdctl::exec(container, command).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::copy | /// Wrapper for nerdctl::copy | ||||||
| /// | /// | ||||||
| /// Copy files between container and local filesystem. | /// Copy files between container and local filesystem. | ||||||
| pub fn nerdctl_copy(source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_copy(source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::copy(source, dest)) |     nerdctl::copy(source, dest).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::stop | /// Wrapper for nerdctl::stop | ||||||
| /// | /// | ||||||
| /// Stop a container. | /// Stop a container. | ||||||
| pub fn nerdctl_stop(container: &str) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_stop(container: &str) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::stop(container)) |     nerdctl::stop(container).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::remove | /// Wrapper for nerdctl::remove | ||||||
| /// | /// | ||||||
| /// Remove a container. | /// Remove a container. | ||||||
| pub fn nerdctl_remove(container: &str) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_remove(container: &str) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::remove(container)) |     nerdctl::remove(container).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::list | /// Wrapper for nerdctl::list | ||||||
| /// | /// | ||||||
| /// List containers. | /// List containers. | ||||||
| pub fn nerdctl_list(all: bool) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_list(all: bool) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::list(all)) |     nerdctl::list(all).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::logs | /// Wrapper for nerdctl::logs | ||||||
| /// | /// | ||||||
| /// Get container logs. | /// Get container logs. | ||||||
| pub fn nerdctl_logs(container: &str) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_logs(container: &str) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::logs(container)) |     nerdctl::logs(container).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| // | // | ||||||
| @@ -443,14 +320,14 @@ pub fn nerdctl_logs(container: &str) -> Result<CommandResult, Box<EvalAltResult> | |||||||
| /// | /// | ||||||
| /// List images in local storage. | /// List images in local storage. | ||||||
| pub fn nerdctl_images() -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_images() -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::images()) |     nerdctl::images().map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::image_remove | /// Wrapper for nerdctl::image_remove | ||||||
| /// | /// | ||||||
| /// Remove one or more images. | /// Remove one or more images. | ||||||
| pub fn nerdctl_image_remove(image: &str) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_image_remove(image: &str) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::image_remove(image)) |     nerdctl::image_remove(image).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::image_push | /// Wrapper for nerdctl::image_push | ||||||
| @@ -460,21 +337,21 @@ pub fn nerdctl_image_push( | |||||||
|     image: &str, |     image: &str, | ||||||
|     destination: &str, |     destination: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::image_push(image, destination)) |     nerdctl::image_push(image, destination).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::image_tag | /// Wrapper for nerdctl::image_tag | ||||||
| /// | /// | ||||||
| /// Add an additional name to a local image. | /// Add an additional name to a local image. | ||||||
| pub fn nerdctl_image_tag(image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_image_tag(image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::image_tag(image, new_name)) |     nerdctl::image_tag(image, new_name).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::image_pull | /// Wrapper for nerdctl::image_pull | ||||||
| /// | /// | ||||||
| /// Pull an image from a registry. | /// Pull an image from a registry. | ||||||
| pub fn nerdctl_image_pull(image: &str) -> Result<CommandResult, Box<EvalAltResult>> { | pub fn nerdctl_image_pull(image: &str) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::image_pull(image)) |     nerdctl::image_pull(image).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::image_commit | /// Wrapper for nerdctl::image_commit | ||||||
| @@ -484,7 +361,7 @@ pub fn nerdctl_image_commit( | |||||||
|     container: &str, |     container: &str, | ||||||
|     image_name: &str, |     image_name: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::image_commit(container, image_name)) |     nerdctl::image_commit(container, image_name).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Wrapper for nerdctl::image_build | /// Wrapper for nerdctl::image_build | ||||||
| @@ -494,7 +371,7 @@ pub fn nerdctl_image_build( | |||||||
|     tag: &str, |     tag: &str, | ||||||
|     context_path: &str, |     context_path: &str, | ||||||
| ) -> Result<CommandResult, Box<EvalAltResult>> { | ) -> Result<CommandResult, Box<EvalAltResult>> { | ||||||
|     nerdctl_error_to_rhai_error(nerdctl::image_build(tag, context_path)) |     nerdctl::image_build(tag, context_path).map_err(Into::into) | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Register Nerdctl module functions with the Rhai engine | /// Register Nerdctl module functions with the Rhai engine | ||||||
| @@ -512,31 +389,13 @@ pub fn register_nerdctl_module(engine: &mut Engine) -> Result<(), Box<EvalAltRes | |||||||
|  |  | ||||||
|     // Register Container constructor |     // Register Container constructor | ||||||
|     engine.register_fn("nerdctl_container_new", container_new); |     engine.register_fn("nerdctl_container_new", container_new); | ||||||
|     engine.register_fn("nerdctl_container_from_image", container_from_image); |      | ||||||
|  |     // Register HealthCheck constructor | ||||||
|  |     engine.register_fn("health_check_new", health_check_new); | ||||||
|  |  | ||||||
|     // Register Container instance methods |     // Register Container instance methods | ||||||
|     engine.register_fn("reset", container_reset); |     engine.register_fn("reset", container_reset); | ||||||
|     engine.register_fn("with_port", container_with_port); |  | ||||||
|     engine.register_fn("with_volume", container_with_volume); |  | ||||||
|     engine.register_fn("with_env", container_with_env); |  | ||||||
|     engine.register_fn("with_network", container_with_network); |  | ||||||
|     engine.register_fn("with_network_alias", container_with_network_alias); |  | ||||||
|     engine.register_fn("with_cpu_limit", container_with_cpu_limit); |  | ||||||
|     engine.register_fn("with_memory_limit", container_with_memory_limit); |  | ||||||
|     engine.register_fn("with_restart_policy", container_with_restart_policy); |  | ||||||
|     engine.register_fn("with_health_check", container_with_health_check); |  | ||||||
|     engine.register_fn("with_ports", container_with_ports); |  | ||||||
|     engine.register_fn("with_volumes", container_with_volumes); |  | ||||||
|     engine.register_fn("with_envs", container_with_envs); |  | ||||||
|     engine.register_fn("with_network_aliases", container_with_network_aliases); |  | ||||||
|     engine.register_fn("with_memory_swap_limit", container_with_memory_swap_limit); |  | ||||||
|     engine.register_fn("with_cpu_shares", container_with_cpu_shares); |  | ||||||
|     engine.register_fn( |  | ||||||
|         "with_health_check_options", |  | ||||||
|         container_with_health_check_options, |  | ||||||
|     ); |  | ||||||
|     engine.register_fn("with_snapshotter", container_with_snapshotter); |  | ||||||
|     engine.register_fn("with_detach", container_with_detach); |  | ||||||
|     engine.register_fn("build", container_build); |     engine.register_fn("build", container_build); | ||||||
|     engine.register_fn("start", container_start); |     engine.register_fn("start", container_start); | ||||||
|     engine.register_fn("stop", container_stop); |     engine.register_fn("stop", container_stop); | ||||||
| @@ -571,11 +430,18 @@ pub fn register_nerdctl_module(engine: &mut Engine) -> Result<(), Box<EvalAltRes | |||||||
|  |  | ||||||
| /// Register Nerdctl module types with the Rhai engine | /// Register Nerdctl module types with the Rhai engine | ||||||
| fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | ||||||
|  |  | ||||||
|     // Register Container type |     // Register Container type | ||||||
|     engine.register_type_with_name::<Container>("NerdctlContainer"); |     engine.register_type_with_name::<Container>("NerdctlContainer"); | ||||||
|  |  | ||||||
|     // Register getters for Container properties |     // Register getters & setters for Container properties | ||||||
|  |     // -- name | ||||||
|     engine.register_get("name", |container: &mut Container| container.name.clone()); |     engine.register_get("name", |container: &mut Container| container.name.clone()); | ||||||
|  |     engine.register_set("image", |container: &mut Container, image: &str| { | ||||||
|  |         container.image = Some(image.to_string()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- container_id | ||||||
|     engine.register_get( |     engine.register_get( | ||||||
|         "container_id", |         "container_id", | ||||||
|         |container: &mut Container| match &container.container_id { |         |container: &mut Container| match &container.container_id { | ||||||
| @@ -583,12 +449,37 @@ fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> | |||||||
|             None => "".to_string(), |             None => "".to_string(), | ||||||
|         }, |         }, | ||||||
|     ); |     ); | ||||||
|  |     engine.register_set("container_id", |container: &mut Container, container_id: &str| { | ||||||
|  |         container.container_id = Some(container_id.to_string()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- image | ||||||
|     engine.register_get("image", |container: &mut Container| { |     engine.register_get("image", |container: &mut Container| { | ||||||
|         match &container.image { |         match &container.image { | ||||||
|             Some(img) => img.clone(), |             Some(img) => img.clone(), | ||||||
|             None => "".to_string(), |             None => "".to_string(), | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  |     engine.register_set("image", |container: &mut Container, image: &str| { | ||||||
|  |         container.image = Some(image.to_string()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- config | ||||||
|  |     engine.register_get("config", |container: &mut Container| { | ||||||
|  |         container | ||||||
|  |             .config | ||||||
|  |             .iter() | ||||||
|  |             .map(|(k, v)| (k.clone().into(), v.clone().into())) | ||||||
|  |             .collect::<Map>() | ||||||
|  |     }); | ||||||
|  |     engine.register_set("config", |container: &mut Container, config: Map| { | ||||||
|  |         container.config = config | ||||||
|  |             .into_iter() | ||||||
|  |             .map(|(k, v)| (k.to_string(), v.into_string().unwrap_or_default())) | ||||||
|  |             .collect(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- ports | ||||||
|     engine.register_get("ports", |container: &mut Container| { |     engine.register_get("ports", |container: &mut Container| { | ||||||
|         let mut array = Array::new(); |         let mut array = Array::new(); | ||||||
|         for port in &container.ports { |         for port in &container.ports { | ||||||
| @@ -596,6 +487,14 @@ fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> | |||||||
|         } |         } | ||||||
|         array |         array | ||||||
|     }); |     }); | ||||||
|  |     engine.register_set("ports", |container: &mut Container, ports: Array| { | ||||||
|  |         container.ports = ports | ||||||
|  |             .into_iter() | ||||||
|  |             .map(|v| v.into_string().unwrap_or_default()) | ||||||
|  |             .collect(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- volumes | ||||||
|     engine.register_get("volumes", |container: &mut Container| { |     engine.register_get("volumes", |container: &mut Container| { | ||||||
|         let mut array = Array::new(); |         let mut array = Array::new(); | ||||||
|         for volume in &container.volumes { |         for volume in &container.volumes { | ||||||
| @@ -603,11 +502,161 @@ fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> | |||||||
|         } |         } | ||||||
|         array |         array | ||||||
|     }); |     }); | ||||||
|  |     engine.register_set("volumes", |container: &mut Container, volumes: Array| { | ||||||
|  |         container.volumes = volumes | ||||||
|  |             .into_iter() | ||||||
|  |             .map(|v| v.into_string().unwrap_or_default()) | ||||||
|  |             .collect(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- env_vars | ||||||
|  |     engine.register_get("env_vars", |container: &mut Container| { | ||||||
|  |         container | ||||||
|  |             .env_vars | ||||||
|  |             .iter() | ||||||
|  |             .map(|(k, v)| (k.clone().into(), v.clone().into())) | ||||||
|  |             .collect::<Map>() | ||||||
|  |     }); | ||||||
|  |     engine.register_set("env_vars", |container: &mut Container, env_vars: Map| { | ||||||
|  |         container.env_vars = env_vars | ||||||
|  |             .into_iter() | ||||||
|  |             .map(|(k, v)| (k.to_string(), v.into_string().unwrap_or_default())) | ||||||
|  |             .collect(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- network | ||||||
|  |     engine.register_get("network", |container: &mut Container| { | ||||||
|  |         container.network.clone().unwrap_or_default() | ||||||
|  |     }); | ||||||
|  |     engine.register_set("network", |container: &mut Container, network: &str| { | ||||||
|  |         container.network = Some(network.to_string()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- network_aliases | ||||||
|  |     engine.register_get("network_aliases", |container: &mut Container| { | ||||||
|  |         container | ||||||
|  |             .network_aliases | ||||||
|  |             .iter() | ||||||
|  |             .map(|alias| Dynamic::from(alias.clone())) | ||||||
|  |             .collect::<Array>() | ||||||
|  |     }); | ||||||
|  |     engine.register_set( | ||||||
|  |         "network_aliases", | ||||||
|  |         |container: &mut Container, aliases: Array| { | ||||||
|  |             container.network_aliases = aliases | ||||||
|  |                 .into_iter() | ||||||
|  |                 .map(|a| a.into_string().unwrap_or_default()) | ||||||
|  |                 .collect(); | ||||||
|  |         }, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // -- cpu_limit | ||||||
|  |     engine.register_get("cpu_limit", |container: &mut Container| { | ||||||
|  |         container.cpu_limit.clone().unwrap_or_default() | ||||||
|  |     }); | ||||||
|  |     engine.register_set("cpu_limit", |container: &mut Container, limit: &str| { | ||||||
|  |         container.cpu_limit = Some(limit.to_string()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- memory_limit | ||||||
|  |     engine.register_get("memory_limit", |container: &mut Container| { | ||||||
|  |         container.memory_limit.clone().unwrap_or_default() | ||||||
|  |     }); | ||||||
|  |     engine.register_set("memory_limit", |container: &mut Container, limit: &str| { | ||||||
|  |         container.memory_limit = Some(limit.to_string()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- memory_swap_limit | ||||||
|  |     engine.register_get("memory_swap_limit", |container: &mut Container| { | ||||||
|  |         container.memory_swap_limit.clone().unwrap_or_default() | ||||||
|  |     }); | ||||||
|  |     engine.register_set( | ||||||
|  |         "memory_swap_limit", | ||||||
|  |         |container: &mut Container, limit: &str| { | ||||||
|  |             container.memory_swap_limit = Some(limit.to_string()); | ||||||
|  |         }, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // -- cpu_shares | ||||||
|  |     engine.register_get("cpu_shares", |container: &mut Container| { | ||||||
|  |         container.cpu_shares.clone().unwrap_or_default() | ||||||
|  |     }); | ||||||
|  |     engine.register_set("cpu_shares", |container: &mut Container, shares: &str| { | ||||||
|  |         container.cpu_shares = Some(shares.to_string()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- restart_policy | ||||||
|  |     engine.register_get("restart_policy", |container: &mut Container| { | ||||||
|  |         container.restart_policy.clone().unwrap_or_default() | ||||||
|  |     }); | ||||||
|  |     engine.register_set( | ||||||
|  |         "restart_policy", | ||||||
|  |         |container: &mut Container, policy: &str| { | ||||||
|  |             container.restart_policy = Some(policy.to_string()); | ||||||
|  |         }, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // -- health_check | ||||||
|  |     engine.register_type_with_name::<HealthCheck>("NerdctlHealthCheck"); | ||||||
|  |     engine.register_get("health_check", |container: &mut Container| { | ||||||
|  |         if let Some(health_check) = container.health_check.clone() { | ||||||
|  |             Dynamic::from(health_check) | ||||||
|  |         } else { | ||||||
|  |             Dynamic::UNIT | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |     engine.register_set( | ||||||
|  |         "health_check", | ||||||
|  |         |container: &mut Container, health_check: HealthCheck| { | ||||||
|  |             container.health_check = Some(health_check); | ||||||
|  |         }, | ||||||
|  |     ); | ||||||
|  |     // Register getters & setters for HealthCheck properties | ||||||
|  |     engine.register_get("cmd", |hc: &mut HealthCheck| hc.cmd.clone()); | ||||||
|  |     engine.register_set("cmd", |hc: &mut HealthCheck, cmd: &str| { | ||||||
|  |         hc.cmd = cmd.to_string(); | ||||||
|  |     }); | ||||||
|  |     engine.register_get("interval", |hc: &mut HealthCheck| { | ||||||
|  |         hc.interval.clone().unwrap_or_default() | ||||||
|  |     }); | ||||||
|  |     engine.register_set("interval", |hc: &mut HealthCheck, interval: &str| { | ||||||
|  |         hc.interval = Some(interval.to_string()); | ||||||
|  |     }); | ||||||
|  |     engine.register_get("timeout", |hc: &mut HealthCheck| { | ||||||
|  |         hc.timeout.clone().unwrap_or_default() | ||||||
|  |     }); | ||||||
|  |     engine.register_set("timeout", |hc: &mut HealthCheck, timeout: &str| { | ||||||
|  |         hc.timeout = Some(timeout.to_string()); | ||||||
|  |     }); | ||||||
|  |     engine.register_get("retries", |hc: &mut HealthCheck| { | ||||||
|  |         hc.retries.map_or(0, |r| r as i64) | ||||||
|  |     }); | ||||||
|  |     engine.register_set("retries", |hc: &mut HealthCheck, retries: i64| { | ||||||
|  |         hc.retries = Some(retries as u32); | ||||||
|  |     }); | ||||||
|  |     engine.register_get("start_period", |hc: &mut HealthCheck| { | ||||||
|  |         hc.start_period.clone().unwrap_or_default() | ||||||
|  |     }); | ||||||
|  |     engine.register_set("start_period", |hc: &mut HealthCheck, start_period: &str| { | ||||||
|  |         hc.start_period = Some(start_period.to_string()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- detach | ||||||
|     engine.register_get("detach", |container: &mut Container| container.detach); |     engine.register_get("detach", |container: &mut Container| container.detach); | ||||||
|  |     engine.register_set("detach", |container: &mut Container, detach: bool| { | ||||||
|  |         container.detach = detach; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // -- snapshotter | ||||||
|  |     engine.register_get("snapshotter", |container: &mut Container| { | ||||||
|  |         container.snapshotter.clone().unwrap_or_default() | ||||||
|  |     }); | ||||||
|  |     engine.register_set("snapshotter", |container: &mut Container, snapshotter: &str| { | ||||||
|  |         container.snapshotter = Some(snapshotter.to_string()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     // Register Image type and methods |     // Register Image type and methods | ||||||
|     engine.register_type_with_name::<Image>("NerdctlImage"); |     engine.register_type_with_name::<Image>("NerdctlImage"); | ||||||
|  |  | ||||||
|     // Register getters for Image properties |     // Register getters for Image properties | ||||||
|     engine.register_get("id", |img: &mut Image| img.id.clone()); |     engine.register_get("id", |img: &mut Image| img.id.clone()); | ||||||
|     engine.register_get("repository", |img: &mut Image| img.repository.clone()); |     engine.register_get("repository", |img: &mut Image| img.repository.clone()); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user