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
-
Proxmox-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.
+
+
+
+
+
+
## 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`.
+
@@ -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"