diff --git a/.gitea/workflows/action.yml b/.gitea/workflows/action.yml index 7494674..0a80483 100644 --- a/.gitea/workflows/action.yml +++ b/.gitea/workflows/action.yml @@ -2,7 +2,7 @@ inputs: repo: required: true type: string - ref: + branch: required: true type: string default: "main" @@ -14,7 +14,7 @@ runs: uses: https://gitea.com/actions/checkout@v4 with: repository: "${{ inputs.repo }}" - ref: "${{ inputs.ref }}" + ref: "${{ inputs.branch }}" path: repo - name: Configure container @@ -52,7 +52,7 @@ runs: uses: https://gitea.com/actions/checkout@v4 with: repository: "${{ inputs.repo }}" - ref: "${{ gitea.ref_name }}" + ref: "${{ inputs.branch }}" path: "${{ env.repo }}" - name: Checkout libraries @@ -62,6 +62,11 @@ runs: ref: 'main' path: "${{ env.repo }}/libraries" + - name: Create Snapshot + if: ${{ inputs.branch == 'snapshot' }} + run: echo "Utils.snapshot(self, node['snapshot']['data'])" > "${{ env.repo }}/recipes/default.rb" + shell: bash + - name: Configure container run: | tar -c "${{ env.repo }}" -cz | ssh -o StrictHostKeyChecking=no -i "/share/.ssh/${{ env.id }}" "config@${{ env.ip }}" 'sudo tar xz -C /tmp diff --git a/.gitea/workflows/pipeline.yml b/.gitea/workflows/pipeline.yml index c384b54..f941783 100644 --- a/.gitea/workflows/pipeline.yml +++ b/.gitea/workflows/pipeline.yml @@ -72,7 +72,7 @@ jobs: tar -c config -cz | ssh -o StrictHostKeyChecking=no -i "/share/.ssh/${id}" "config@${ip}" 'sudo tar xz -C /tmp sudo -E IP="'"${ip}"'" ID="'"${id}"'" ENDPOINT=${{ vars.ENDPOINT }} LOGIN="'"${login}"'" PASSWORD="'"${password}"'" \ cinc-client --local-mode --config-option cookbook_path="[\"/tmp/config\", \"/tmp/config/libs\"]" -o share' - if: ${{ gitea.ref != 'refs/heads/release' || success() }} + if: ${{ gitea.ref == 'refs/heads/release' }} config: runs-on: [ "shell" ] diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..752c0be --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +report@gitops.pm. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..7a1d83e --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing to Proxmox-GitOps + +Thank you for considering contributing to Proxmox-GitOps. + +This document provides guidelines for contributing. These are conventions, not strict mandates; feel free to propose improvements to this document via a pull request. + +This project is governed by the [Code of Conduct](.github/CODE_OF_CONDUCT.md) and released under the [MIT License](LICENSE). By participating, contributors agree to uphold these terms. + +### Workflow +- Branching: Fork the repository and create a branch from `develop`. The `main` branch is for stable releases, while `develop` is the active integration branch. +- Make Changes: Follow existing patterns in the codebase. Keep the branch narrowly scoped for easier review. +- Idempotency: Test changes multiple times to ensure idempotency. Subsequent runs should result in no changes. +- Open Pull Request: Open a pull request from the fork’s branch to the main repository’s `develop` branch. Provide a clear summary and link relevant issues. + +### Development Guidelines + +#### Architecture +- Proxmox-GitOps is a self-contained monorepo that uses Git submodules to compose the complete Infrastructure-as-Code declaration. +- The system bootstraps from a local Docker environment, which initializes itself. + +#### Idiomatic Development +- Abstraction and modulararity: Extract repetitive tasks into high-level modules. Prefer shared, project-specific abstractions over re-defining primitive resource to enforce consistency and centralize logic. +- Context (`ctx`): Most library expect a context object (`ctx`) as argument. Within configuration, pass `self` as context to provide access to the run context, node attributes, and the resource DSL. +- Centralize Configuration: Encapsulate lookup to reinforce GitOps-driven model with centrally managed configuration. +- Preserve Separation of Concerns. + +### Container Definitions +Modular container definitions in `libs/` following this structure: + +``` +libs/mycontainer/ +├── config.env +├── recipes/ +│ └── default.rb +├── templates/ +└── attributes/ + └── default.rb +``` + +### Questions and Support +- Issues: Use GitHub Issues for bug reports and feature requests. +- Report Bugs + - Include clear, step-by-step instructions to reproduce the issue. + - Describe the expected behavior versus the actual behavior. + - Add relevant environment details (e.g., versions). + - Attach applicable logs. +- Suggest Enhancements + - Explain change and motivation. + - Describe how it aligns with the project's scope, concept and architecture. +- Discussion: Engage with maintainers and contributors. +- Documentation: [Wiki](https://github.com/stevius10/Proxmox-GitOps/wiki) for setup instructions and configuration examples. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8594018..842ba3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,9 @@ on: pull_request: branches: [ main, develop ] +permissions: + contents: read + jobs: init: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 1bba6eb..30c665b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - [Trade-offs](#trade-offs) - [Usage](#usage) - [Lifecycle](#lifecycle) - - [Self-contained Monorepository](#self-contained-monorepository) + - [Self-Containment](#self-containment) - [Requirements](#requirements) - [Configuration](#configuration) - [Development and Extension](#development-and-extension) @@ -21,9 +21,15 @@ ## Overview -MonorepoProxmox-GitOps implements a self-sufficient, extensible CI/CD environment for provisioning, configuring, and orchestrating Linux Containers (LXC) within Proxmox VE.

-Leveraging an Infrastructure-as-Code (IaC) approach, it manages the entire container lifecycle—bootstrapping, deployment, configuration, and validation—through version-controlled automation. -
+Proxmox-GitOps implements a self-contained GitOps environment for provisioning and orchestrating Linux Containers (LXC) on Proxmox VE. + +Encapsulating infrastructure within an extensible monorepository — recursively resolved from Git submodules at runtime — it provides a comprehensive Infrastructure-as-Code (IaC) abstraction for an entire, automated container-based infrastructure. + +


+ + Demo + +


## Architecture @@ -44,9 +50,9 @@ This system implements stateless infrastructure management on Proxmox VE, ensuri | Concept | Approach | Reasoning | |---------|----------|-----------| | **Ephemeral State** | Git repository represents *current desired state*, ensuring state purity across deployments.| Deployment consistency and stateless infrastructure over version history. | -| **Recursive Self-Containment** | Embedded control plane recursively provisions itself within target containers, ensuring deterministic bootstrap.| Prevents configuration drift; enables consistent and reproducible behavior. | -| **Dynamic Orchestration** | Imperative logic (e.g. `config/recipes/repo.rb`) used for dynamic, cross-layer state management| Declarative approach intractable for adjusting to dynamic cross-layer changes (e.g. submodule remote rewriting). | -| **Monorepository** | Centralizes infrastructure as a single code artifact; submodules modularize development at runtime | Consistency and modularity: infrastructure self-contained; dynamically resolved in recursive context. | +| **Recursive Self-Containment** | Control plane seeds itself by pushing its monorepository onto a locally bootstrapped instance, triggering a pipeline that recursively provisions the control plane onto PVE.| Environmental parity for local and PVE, enabling one-click deployment from version-controlled monorepository. Reuse of validated, generic base. +| **Dynamic Orchestration** | Imperative logic (e.g. `config/recipes/repo.rb`) used for dynamic, cross-layer state management.| Declarative approach intractable for adjusting to dynamic cross-layer changes (e.g. submodule remote rewriting). | +| **Monorepository** | Centralizes infrastructure as single code artifact, using submodules for modular composition.| Consistency and modularity: infrastructure self-contained; dynamically resolved in recursive context. | ### Design @@ -54,6 +60,8 @@ This system implements stateless infrastructure management on Proxmox VE, ensuri - **Headless container configuration:** By convention, Ansible is used for provisioning (`community.proxmox` upstream); Cinc (Chef) handles modular, recursive desired state complexity. +- **Integrated Baseline:** The `base` role standardizes defaults in container configuration. The control plane leverages this baseline and uses built-in infrastructure libraries to deploy itself recursively, establishing an operational pattern that is reproduced in container `libs`. +


Recursive deployment @@ -72,16 +80,16 @@ This system implements stateless infrastructure management on Proxmox VE, ensuri ### Lifecycle -#### Self-contained Monorepository +#### Self-Containment -`git clone --recurse-submodules`, e.g. **Version-Controlled Mirroring** +`git clone --recurse-submodules`, e.g. for **Version-Controlled Mirroring** -- **Backup**: See [Self-contained Monorepository](#self-contained-monorepository) - - use `local/share` for persistence or self-reference network share +- **Backup**: See [Self-Containment](#self-containment) + - use `local/share/` [for persistence](https://github.com/stevius10/Proxmox-GitOps/wiki/State-and-Persistence) or self-reference network share -- **Update**: See [Self-contained Monorepository](#self-contained-monorepository), and redeploy merged +- **Update**: See [Self-Containment](#self-containment), and redeploy merged -- **Rollback**: See [Self-contained Monorepository](#self-contained-monorepository), or set `snapshot` branch to `release` at runtime +- **Rollback**: See [Self-Containment](#self-containment), or push `rollback` to `release` at runtime *Appendix*: The self-referential language in this section is intentional. It mirrors the system's recursive architecture, implying lifecycle operations emerge from the principle itself. diff --git a/base/default.yml b/base/default.yml index 140cdcf..002a14b 100644 --- a/base/default.yml +++ b/base/default.yml @@ -50,6 +50,7 @@ password: "{{ lookup('env', 'PASSWORD') }}" mount: "{{ mount }}" when: + - lookup('env','PROXMOX_PASSWORD') | default('', True) | length > 0 - not (share | default(false) | bool) - mount is defined tags: mounts diff --git a/base/roles/container/defaults/main.yml b/base/roles/container/defaults/main.yml index 28f360c..a064021 100644 --- a/base/roles/container/defaults/main.yml +++ b/base/roles/container/defaults/main.yml @@ -18,3 +18,7 @@ proxmox_cred: check_delay: 4 check_retries: 15 + +network_gateway: "192.168.178.1" +network_netmask: "24" +network_bridge: "vmbr0" diff --git a/base/roles/container/tasks/create.yml b/base/roles/container/tasks/create.yml index ccefc87..7cbc4d2 100644 --- a/base/roles/container/tasks/create.yml +++ b/base/roles/container/tasks/create.yml @@ -34,7 +34,7 @@ set_fact: features: "{{ features + ['mount=cifs'] }}" when: - - share | default(false) | bool + - not share | default(false) | bool - mount | default('') | trim != '' - PROXMOX_PASSWORD is defined - PROXMOX_PASSWORD != '' @@ -49,11 +49,11 @@ pubkey: "{{ lookup('file', [key_dir, id ~ '.pub'] | path_join) }}" swap: "{{ swap }}" disk: "{{ disk }}" + netif: + net0: "name=eth0,gw={{ network_gateway }},ip={{ ip }}/{{ network_netmask }},bridge={{ network_bridge }}" features: "{{ (features if features and (PROXMOX_PASSWORD is defined and PROXMOX_PASSWORD != '') else omit) }}" mounts: "{{ (mounts if mounts and (PROXMOX_PASSWORD is defined and PROXMOX_PASSWORD != '') else omit) }}" unprivileged: "{{ (share | default(false) and mount | default('') | trim != '') | ternary(false, true) }}" - netif: - net0: "name=eth0,gw=192.168.178.1,ip={{ ip }}/24,bridge=vmbr0" onboot: "{{ boot }}" state: present register: container_creation diff --git a/base/roles/mount/tasks/main.yml b/base/roles/mount/tasks/main.yml index eddb1e2..9435d5b 100644 --- a/base/roles/mount/tasks/main.yml +++ b/base/roles/mount/tasks/main.yml @@ -9,6 +9,16 @@ mount_list: "{{ mount.split(',') | reject('equalto', '') | select('match', '^[A-Za-z0-9_]+$') | list }}" when: mount is defined +- name: Create credentials file + ansible.builtin.copy: + dest: /root/.share + content: | + username={{ login }} + password={{ password }} + owner: root + group: root + mode: '0600' + - name: Ensure mount directories exist ansible.builtin.file: path: "{{ '/share' if item == 'share' else '/share/' ~ item }}" @@ -20,17 +30,37 @@ loop_control: label: "{{ item }}" -- name: Enable mount - ansible.posix.mount: - src: "//{{ host }}/{{ item }}" - path: "{{ '/share' if item == 'share' else '/share/' ~ item }}" - fstype: cifs - opts: "username={{ login }},password={{ password }},uid=1000,gid=1000,vers=3.0" - state: mounted +- name: Create mount service + ansible.builtin.template: + src: share.service.j2 + dest: "/etc/systemd/system/{{ ('share' if item == 'share' else 'share-' ~ item) }}.service" + owner: root + group: root + mode: '0644' + loop: "{{ mount_list }}" + loop_control: + label: "{{ item }}" + +- name: Create mount timer + ansible.builtin.template: + src: share.timer.j2 + dest: "/etc/systemd/system/{{ ('share' if item == 'share' else 'share-' ~ item) }}.timer" + owner: root + group: root + mode: '0644' loop: "{{ mount_list }}" loop_control: label: "{{ item }}" -- name: Restart mount - ansible.builtin.command: mount -a - ignore_errors: yes # avoid permission error if token usage +- name: Reload systemd + ansible.builtin.systemd_service: + daemon_reload: true + +- name: Enable mount service + ansible.builtin.systemd: + name: "{{ ('share' if item == 'share' else 'share-' ~ item) }}.timer" + enabled: yes + state: started + loop: "{{ mount_list }}" + loop_control: + label: "{{ item }}" diff --git a/base/roles/mount/templates/share.service.j2 b/base/roles/mount/templates/share.service.j2 new file mode 100644 index 0000000..d83db55 --- /dev/null +++ b/base/roles/mount/templates/share.service.j2 @@ -0,0 +1,13 @@ +{% set mount_path = '/share' if item == 'share' else '/share/' ~ item %} +{% set unit_name = 'share' if item == 'share' else 'share-' ~ item %} +[Unit] +Description=mount {{ item }} to {{ mount_path }} +After=network.target +StartLimitIntervalSec=900 +StartLimitBurst=10 + +[Service] +Type=oneshot +ExecStart=/usr/bin/mount -t cifs //{{ host }}/{{ item }} {{ mount_path }} -o credentials=/root/.share,uid=1000,gid=1000,vers=3.0 +ExecStartPost=/usr/bin/bash -c 'mountpoint -q {{ mount_path }} && /usr/bin/systemctl stop --now {{ unit_name }}.timer || true' +ExecStopPost=/usr/bin/bash -c '[ "$SERVICE_RESULT" = "start-limit-hit" ] && /usr/bin/systemctl stop --now {{ unit_name }}.timer || true' \ No newline at end of file diff --git a/base/roles/mount/templates/share.timer.j2 b/base/roles/mount/templates/share.timer.j2 new file mode 100644 index 0000000..b3baa75 --- /dev/null +++ b/base/roles/mount/templates/share.timer.j2 @@ -0,0 +1,11 @@ +{% set unit_name = 'share' if item == 'share' else 'share-' ~ item %} +[Unit] +Description=mount {{ item }} timer + +[Timer] +OnBootSec=30s +OnUnitActiveSec=3min +Unit={{ unit_name }}.service + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/config/attributes/default.rb b/config/attributes/default.rb index bf0c4aa..f1aef95 100644 --- a/config/attributes/default.rb +++ b/config/attributes/default.rb @@ -1,34 +1,36 @@ -default['id'] = ENV['ID'] -default['host'] = (default['ip'] = ENV['IP'].to_s.presence || "127.0.0.1") -default['key'] = ENV['KEY'].to_s.presence || "/share/.ssh/#{node['id']}" +default['id'] = ENV['ID'] +default['host'] = (default['ip'] = ENV['IP'].to_s.presence || "127.0.0.1") +default['key'] = ENV['KEY'].to_s.presence || "/share/.ssh/#{node['id']}" -default['app']['user'] = Default.user(node, default: true) -default['app']['group'] = Default.group(node, default: true) -default['app']['config'] = Default.config(node, default: true) +default['app']['user'] = Default.user(node, default: true) +default['app']['group'] = Default.group(node, default: true) +default['app']['config'] = Default.config(node, default: true) -default['git']['conf']['customize'] = true -default['git']['conf']['repo'] = [ "./", "./base", "./config/libraries", "./libs" ] +default['git']['conf']['customize'] = true +default['git']['conf']['repo'] = [ "./", "./base", "./config/libraries", "./libs" ] -default['git']['dir']['app'] = '/app/git' -default['git']['dir']['home'] = Dir.home(node['app']['user']) || ENV['HOME'] || '/app' -default['git']['dir']['custom'] = "#{node['git']['dir']['app']}/custom" -default['git']['dir']['workspace'] = "#{node['git']['dir']['home']}/workspace" +default['git']['dir']['app'] = '/app/git' +default['git']['dir']['home'] = Dir.home(node['app']['user']) || ENV['HOME'] || '/app' +default['git']['dir']['custom'] = "#{node['git']['dir']['app']}/custom" +default['git']['dir']['workspace'] = "#{node['git']['dir']['home']}/workspace" -default['git']['port']['http'] = 8080 -default['git']['port']['ssh'] = 2222 -default['git']['host']['http'] = "http://#{node['host']}:#{node['git']['port']['http']}" -default['git']['host']['ssh'] = "#{node['host']}:#{node['git']['port']['ssh']}" +default['git']['port']['http'] = 8080 +default['git']['port']['ssh'] = 2222 +default['git']['host']['http'] = "http://#{node['host']}:#{node['git']['port']['http']}" +default['git']['host']['ssh'] = "#{node['host']}:#{node['git']['port']['ssh']}" -default['git']['api']['version'] = "v1" -default['git']['api']['endpoint'] = "http://#{node['host']}:#{node['git']['port']['http']}/api/#{node['git']['api']['version']}" +default['git']['api']['version'] = "v1" +default['git']['api']['endpoint'] = "http://#{node['host']}:#{node['git']['port']['http']}/api/#{node['git']['api']['version']}" -default['git']['org']['main'] = 'main' -default['git']['org']['stage'] = 'stage' -default['git']['org']['tasks'] = 'tasks' +default['git']['org']['main'] = 'main' +default['git']['org']['stage'] = 'stage' +default['git']['org']['tasks'] = 'tasks' + +default['git']['branch']['rollback'] = 'rollback' # Runner -default['runner']['dir']['app'] = '/app/runner' -default['runner']['dir']['cache'] = '/tmp' +default['runner']['dir']['app'] = '/app/runner' +default['runner']['dir']['cache'] = '/tmp' -default['runner']['conf']['label'] = 'shell' +default['runner']['conf']['label'] = 'shell' diff --git a/config/libraries/common.rb b/config/libraries/common.rb index 9810f37..1485b79 100644 --- a/config/libraries/common.rb +++ b/config/libraries/common.rb @@ -34,9 +34,9 @@ def self.daemon(ctx, name) end def self.application(ctx, name, user: nil, group: nil, - exec: nil, cwd: nil, unit: {}, actions: [:enable, :start], - restart: 'on-failure', subscribe: nil, reload: 'systemd_reload', verify: true, - verify_timeout: 60, verify_interval: 3, verify_cmd: "systemctl is-active --quiet #{name}") + exec: nil, cwd: nil, unit: {}, actions: [:enable, :start], subscribe: nil, reload: 'systemd_reload', + restart: 'on-failure', restart_delay: 10, restart_limit: 10, restart_max: 600, + verify: true, verify_timeout: 60, verify_interval: 5, verify_cmd: "systemctl is-active --quiet #{name}") user ||= Default.user(ctx) group ||= Default.group(ctx) user = user.to_s @@ -45,11 +45,20 @@ def self.application(ctx, name, user: nil, group: nil, if exec daemon(ctx, reload) - service = {'Type' => 'simple', 'User' => user, 'Group' => group, 'Restart' => restart } - service['ExecStart'] = exec if exec - service['WorkingDirectory'] = cwd if cwd - defaults = { 'Unit' => { 'Description' => name.capitalize, 'After' => 'network.target' }, - 'Service' => service, 'Install' => { 'WantedBy' => 'multi-user.target' } } + defaults = { + 'Unit' => { + 'Description' => name.capitalize, 'After' => 'network.target', + 'StartLimitBurst' => restart_limit, + 'StartLimitIntervalSec' => restart_max + }, + 'Service' => ( { + 'Type' => 'simple', 'User' => user, 'Group' => group, + 'Restart' => restart, + 'RestartSec' => restart_delay + }.merge(exec ? { 'ExecStart' => exec } : {}) + .merge(cwd ? { 'WorkingDirectory' => cwd } : {}) ), + 'Install' => { 'WantedBy' => 'multi-user.target' } + } unit_config = defaults.dup unit.each { |section, settings| unit_config[section] = (unit_config[section] || {}).merge(settings) } @@ -80,7 +89,7 @@ def self.application(ctx, name, user: nil, group: nil, if actions.include?(:force_restart) Ctx.dsl(ctx).execute "force_restart_#{name}" do - command "systemctl stop #{name} || true && sleep 1 && systemctl start #{name}" + command "systemctl reset-failed #{name}; systemctl stop #{name} || true && sleep 1 && systemctl start #{name}" action :run end else diff --git a/config/libraries/utils.rb b/config/libraries/utils.rb index 6e70512..e16c259 100644 --- a/config/libraries/utils.rb +++ b/config/libraries/utils.rb @@ -69,7 +69,7 @@ def self.snapshot(ctx, dir, snapshot_dir: '/share/snapshots', name: ctx.cookbook snapshot = File.join(snapshot_dir, name, "#{name}-#{timestamp}.tar.gz") md5_dir = ->(path) { entries = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH) - files = entries.reject { |f| File.directory?(f) || ['.', '..'].include?(File.basename(f)) || File.basename(f).start_with?('._') } + files = entries.reject { |f| File.directory?(f) || File.symlink?(f) || ['.', '..'].include?(File.basename(f)) || File.basename(f).start_with?('._') } Digest::MD5.new.tap { |md5| files.sort.each { |f| File.open(f, 'rb') { |io| md5.update(io.read) } } }.hexdigest } verify = ->(archive, compare_dir) { Dir.mktmpdir do |tmp| diff --git a/config/recipes/repo/push.rb b/config/recipes/repo/push.rb index 5f0a546..4827c4b 100644 --- a/config/recipes/repo/push.rb +++ b/config/recipes/repo/push.rb @@ -26,9 +26,9 @@ execute "repo_#{name_repo}_push_snapshot" do command <<-EOH cp -r #{path_destination}/.git #{path_working} - cd #{path_working} && git checkout -b snapshot && git add -A - git commit --allow-empty -m "snapshot [skip ci]" - git push -f origin snapshot && (rm -rf #{path_working} || true) + cd #{path_working} && git checkout -b #{node['git']['branch']['rollback']} && git add -A + git commit --allow-empty -m "[skip ci]" + git push -f origin #{node['git']['branch']['rollback']} && (rm -rf #{path_working} || true) EOH cwd path_destination user node['app']['user'] diff --git a/config/templates/repo_pipeline.yml.erb b/config/templates/repo_pipeline.yml.erb index ff44609..c9aa23c 100644 --- a/config/templates/repo_pipeline.yml.erb +++ b/config/templates/repo_pipeline.yml.erb @@ -1,7 +1,7 @@ on: workflow_dispatch: push: - branches: [ release, main, develop ] + branches: [ release, main ] jobs: include: @@ -11,4 +11,4 @@ jobs: uses: main/config/.gitea/workflows@main with: repo: ${{ gitea.repository }} - ref: ${{ gitea.ref_name }} + branch: ${{ gitea.ref_name }} diff --git a/docs/demo.gif b/docs/demo.gif new file mode 100644 index 0000000..70067c5 Binary files /dev/null and b/docs/demo.gif differ diff --git a/docs/img/nutshell.png b/docs/img/nutshell.png index 74a0887..83dd0bc 100644 Binary files a/docs/img/nutshell.png and b/docs/img/nutshell.png differ diff --git a/libs/assistant/attributes/default.rb b/libs/assistant/attributes/default.rb index 37782ff..9c04ccf 100644 --- a/libs/assistant/attributes/default.rb +++ b/libs/assistant/attributes/default.rb @@ -6,4 +6,6 @@ default['assistant']['dir']['env'] = '/app/venv' default['assistant']['dir']['data'] = '/app/assistant' -default['configurator']['dir'] = '/app/configurator' \ No newline at end of file +default['configurator']['dir'] = '/app/configurator' + +default['snapshot']['data'] = node['assistant']['dir']['data'] diff --git a/libs/assistant/recipes/default.rb b/libs/assistant/recipes/default.rb index ef44fcb..c8a843e 100644 --- a/libs/assistant/recipes/default.rb +++ b/libs/assistant/recipes/default.rb @@ -42,7 +42,7 @@ end ruby_block "restore_snapshot_if_exists" do - block { Utils.snapshot(self, node['assistant']['dir']['data'], restore: true) } + block { Utils.snapshot(self, node['snapshot']['data'], restore: true) } end Common.application(self, cookbook_name, cwd: node['assistant']['dir']['data'], diff --git a/libs/bridge/attributes/default.rb b/libs/bridge/attributes/default.rb index 5b89c61..34de082 100644 --- a/libs/bridge/attributes/default.rb +++ b/libs/bridge/attributes/default.rb @@ -10,3 +10,5 @@ default['bridge']['dir'] = '/app/bridge' default['bridge']['data'] = "#{node['bridge']['dir']}/data" default['bridge']['logs'] = "#{node['bridge']['dir']}/logs" + +default['snapshot']['data'] = node['bridge']['data'] diff --git a/libs/bridge/recipes/default.rb b/libs/bridge/recipes/default.rb index f393f0d..382ac7b 100644 --- a/libs/bridge/recipes/default.rb +++ b/libs/bridge/recipes/default.rb @@ -66,7 +66,7 @@ end ruby_block "restore_snapshot_if_exists" do - block { Utils.snapshot(self, node['bridge']['data'], restore: true) } + block { Utils.snapshot(self, node['snapshot']['data'], restore: true) } end end diff --git a/libs/bridge/templates/configuration.yaml.erb b/libs/bridge/templates/configuration.yaml.erb index 6730e61..a5c542e 100644 --- a/libs/bridge/templates/configuration.yaml.erb +++ b/libs/bridge/templates/configuration.yaml.erb @@ -20,6 +20,9 @@ advanced: pan_id: GENERATE ext_pan_id: GENERATE + cache_state: true + cache_state_persistent: false + cache_state_send_on_startup: false log_directory: <%= @logs_dir %>/%TIMESTAMP% log_rotation: true log_level: info diff --git a/libs/share/templates/smb.conf.erb b/libs/share/templates/smb.conf.erb index 2a9bf90..af18e5b 100644 --- a/libs/share/templates/smb.conf.erb +++ b/libs/share/templates/smb.conf.erb @@ -18,8 +18,6 @@ fruit:resource = file fruit:model = MacSamba fruit:locking = none -dirsort = yes - fruit:posix_rename = yes fruit:veto_appledouble = no fruit:nfs_aces = no diff --git a/local/run.sh b/local/run.sh index 8fa7ab3..396e8aa 100755 --- a/local/run.sh +++ b/local/run.sh @@ -59,7 +59,8 @@ log "container" "started:${CONTAINER_ID}" sync() { log "sync" "files" - docker cp "$PROJECT_DIR/." "$CONTAINER_ID:/tmp/config/" || fail "sync_failed" + docker exec "$CONTAINER_ID" bash -c "rm -rf /tmp/config/*" || log "sync" "remove files failed" + docker cp "$PROJECT_DIR/." "$CONTAINER_ID:/tmp/config/" || fail "sync" if [[ -n "${COOKBOOK_OVERRIDE}" ]]; then log "sync" "libraries"