/ pkg / system / nix / nix-patch.go
nix-patch.go
  1  package nix
  2  
  3  import (
  4  	"bytes"
  5  	"crypto/rand"
  6  	_ "embed"
  7  	"encoding/hex"
  8  	"errors"
  9  	"fmt"
 10  	"io"
 11  	"log"
 12  	"os"
 13  	"path/filepath"
 14  	"text/template"
 15  	"time"
 16  
 17  	dogeboxd "github.com/dogeorg/dogeboxd/pkg"
 18  )
 19  
 20  //go:embed templates/pup_container.nix
 21  var rawPupContainerTemplate []byte
 22  
 23  //go:embed templates/system_container_config.nix
 24  var rawSystemContainerConfigTemplate []byte
 25  
 26  //go:embed templates/firewall.nix
 27  var rawFirewallTemplate []byte
 28  
 29  //go:embed templates/system.nix
 30  var rawSystemTemplate []byte
 31  
 32  //go:embed templates/dogebox.nix
 33  var rawIncludesFileTemplate []byte
 34  
 35  //go:embed templates/network.nix
 36  var rawNetworkTemplate []byte
 37  
 38  //go:embed templates/storage-overlay.nix
 39  var rawStorageOverlayTemplate []byte
 40  
 41  const (
 42  	NixPatchStatePending     string = "pending"
 43  	NixPatchStateCancelled   string = "cancelled"
 44  	NixPatchStateApplying    string = "applying"
 45  	NixPatchStateApplied     string = "applied"
 46  	NixPatchStateRollingBack string = "rolling back"
 47  	NixPatchStateErrored     string = "errored"
 48  )
 49  
 50  var _ dogeboxd.NixPatch = &nixPatch{}
 51  
 52  type PatchOperation struct {
 53  	Name      string
 54  	Operation func() error
 55  }
 56  
 57  type nixPatch struct {
 58  	id          string
 59  	nm          nixManager
 60  	snapshotDir string
 61  	state       string
 62  	operations  []PatchOperation
 63  	error       error
 64  	log         dogeboxd.SubLogger
 65  }
 66  
 67  func NewNixPatch(nm nixManager, log dogeboxd.SubLogger) dogeboxd.NixPatch {
 68  	id := make([]byte, 6)
 69  	rand.Read(id)
 70  	patchID := hex.EncodeToString(id)
 71  
 72  	p := &nixPatch{
 73  		id:    patchID,
 74  		nm:    nm,
 75  		state: NixPatchStatePending,
 76  		log:   log,
 77  	}
 78  
 79  	log.Logf("[patch-%s] Created new nix patch", p.id)
 80  
 81  	return p
 82  }
 83  
 84  func (np *nixPatch) State() string {
 85  	return np.state
 86  }
 87  
 88  func (np *nixPatch) add(name string, op func() error) error {
 89  	if np.state != NixPatchStatePending {
 90  		return errors.New("patch already applied or cancelled")
 91  	}
 92  
 93  	np.log.Logf("[patch-%s] Adding pending operation %s", np.id, name)
 94  	np.operations = append(np.operations, PatchOperation{Name: name, Operation: op})
 95  
 96  	return nil
 97  }
 98  
 99  func (np *nixPatch) Apply() error {
100  	return np.ApplyCustom(dogeboxd.NixPatchApplyOptions{})
101  }
102  
103  func (np *nixPatch) ApplyCustom(options dogeboxd.NixPatchApplyOptions) error {
104  	if np.state != NixPatchStatePending {
105  		return errors.New("patch already applied or cancelled")
106  	}
107  
108  	np.log.Logf("[patch-%s] Applying nix patch with %d operations", np.id, len(np.operations))
109  
110  	np.state = NixPatchStateApplying
111  
112  	if err := np.snapshot(); err != nil {
113  		np.state = NixPatchStateErrored
114  		np.error = err
115  		return fmt.Errorf("failed to snapshot: %w", err)
116  	}
117  
118  	np.state = NixPatchStateApplying
119  
120  	for _, operation := range np.operations {
121  		np.log.Logf("[patch-%s] Applying operation: %s", np.id, operation.Name)
122  		if err := operation.Operation(); err != nil {
123  			return np.triggerRollback(err)
124  		}
125  	}
126  
127  	if !options.DangerousNoRebuild {
128  		np.log.Logf("[patch-%s] Applied all patch operations, rebuilding..", np.id)
129  
130  		var rebuildFn func(dogeboxd.SubLogger) error
131  
132  		if options.RebuildBoot {
133  			rebuildFn = np.nm.RebuildBoot
134  		} else {
135  			rebuildFn = np.nm.Rebuild
136  		}
137  
138  		if err := rebuildFn(np.log); err != nil {
139  			// We failed.
140  			// Roll back our changes.
141  			np.log.Errf("[patch-%s] Failed to rebuild, rolling back.. %v", np.id, err)
142  			return np.triggerRollback(err)
143  		}
144  	} else {
145  		np.log.Logf("[patch-%s] Applied all patch operations, but not rebuilding as requested.", np.id)
146  	}
147  
148  	if err := os.RemoveAll(np.snapshotDir); err != nil {
149  		np.log.Errf("[patch-%s] Warning: Failed to remove snapshot directory: %v", np.id, err)
150  	} else {
151  		np.log.Logf("[patch-%s] Removed snapshot directory: %s", np.id, np.snapshotDir)
152  	}
153  
154  	np.state = NixPatchStateApplied
155  	np.log.Logf("[patch-%s] Nix patch applied successfully", np.id)
156  
157  	return nil
158  }
159  
160  func (np *nixPatch) Cancel() error {
161  	if np.state != NixPatchStatePending {
162  		return errors.New("patch already applied or cancelled")
163  	}
164  
165  	np.state = NixPatchStateCancelled
166  	return nil
167  }
168  
169  func (np *nixPatch) snapshot() error {
170  	timestamp := time.Now().Unix()
171  
172  	snapshotDir := filepath.Join(np.nm.config.TmpDir, fmt.Sprintf("nix-patch-%d", timestamp))
173  	err := os.MkdirAll(snapshotDir, 0750)
174  	if err != nil {
175  		np.state = NixPatchStateErrored
176  		np.error = err
177  		return fmt.Errorf("failed to create snapshot directory: %w", err)
178  	}
179  
180  	log.Printf("[patch-%s] Snapshotting nix directory to %s", np.id, snapshotDir)
181  
182  	np.snapshotDir = snapshotDir
183  	return copyDirectory(np.nm.config.NixDir, np.snapshotDir)
184  }
185  
186  func (np *nixPatch) triggerRollback(err error) error {
187  	log.Printf("[patch-%s] Triggering rollback", np.id)
188  	log.Printf("[patch-%s] Rollback triggered because of error: %v", np.id, err)
189  
190  	np.state = NixPatchStateRollingBack
191  	np.error = err
192  
193  	if err := np.doRollback(); err != nil {
194  		log.Printf("[patch-%s] Failed to actually roll back: %v", np.id, err)
195  		return fmt.Errorf("failed to actually roll back: %w", err)
196  	}
197  
198  	np.state = NixPatchStateErrored
199  	return err
200  }
201  
202  func (np *nixPatch) doRollback() error {
203  	if np.state != NixPatchStateApplying {
204  		return nil
205  	}
206  
207  	np.state = NixPatchStateRollingBack
208  
209  	err := os.RemoveAll(np.nm.config.NixDir)
210  	if err != nil {
211  		return fmt.Errorf("failed to remove nixDir: %w", err)
212  	}
213  
214  	return copyDirectory(np.snapshotDir, np.nm.config.NixDir)
215  }
216  
217  func (np *nixPatch) UpdateSystemContainerConfiguration(values dogeboxd.NixSystemContainerConfigTemplateValues) {
218  	np.add("UpdateSystemContainerConfiguration", func() error {
219  		return np.writeTemplate("system_container_config.nix", rawSystemContainerConfigTemplate, values)
220  	})
221  }
222  
223  func (np *nixPatch) UpdateFirewall(values dogeboxd.NixFirewallTemplateValues) {
224  	np.add("UpdateFirewall", func() error {
225  		return np.writeTemplate("firewall.nix", rawFirewallTemplate, values)
226  	})
227  }
228  
229  func (np *nixPatch) UpdateSystem(values dogeboxd.NixSystemTemplateValues) {
230  	np.add("UpdateSystem", func() error {
231  		return np.writeTemplate("system.nix", rawSystemTemplate, values)
232  	})
233  }
234  
235  func (np *nixPatch) UpdateNetwork(values dogeboxd.NixNetworkTemplateValues) {
236  	np.add("UpdateNetwork", func() error {
237  		return np.writeTemplate("network.nix", rawNetworkTemplate, values)
238  	})
239  }
240  
241  func (np *nixPatch) UpdateIncludesFile(values dogeboxd.NixIncludesFileTemplateValues) {
242  	np.add("UpdateIncludesFile", func() error {
243  		return np.writeTemplate("dogebox.nix", rawIncludesFileTemplate, values)
244  	})
245  }
246  
247  func (np *nixPatch) WritePupFile(pupId string, values dogeboxd.NixPupContainerTemplateValues) {
248  	np.add("WritePupFile", func() error {
249  		filename := fmt.Sprintf("pup_%s.nix", pupId)
250  		return np.writeTemplate(filename, rawPupContainerTemplate, values)
251  	})
252  }
253  
254  func (np *nixPatch) UpdateStorageOverlay(values dogeboxd.NixStorageOverlayTemplateValues) {
255  	np.add("UpdateStorageOverlay", func() error {
256  		return np.writeTemplate("storage-overlay.nix", rawStorageOverlayTemplate, values)
257  	})
258  }
259  
260  func (np *nixPatch) writeTemplate(filename string, _template []byte, values interface{}) error {
261  	template, err := template.New(filename).Parse(string(_template))
262  	if err != nil {
263  		return err
264  	}
265  
266  	var contents bytes.Buffer
267  	err = template.Execute(&contents, values)
268  	if err != nil {
269  		return err
270  	}
271  
272  	err = np.writeDogeboxNixFile(filename, contents.String())
273  	if err != nil {
274  		return err
275  	}
276  
277  	return nil
278  }
279  
280  func (np *nixPatch) RemovePupFile(pupId string) {
281  	np.add("RemovePupFile", func() error {
282  		// Remove pup nix file
283  		filename := fmt.Sprintf("pup_%s.nix", pupId)
284  		if _, err := os.Stat(filepath.Join(np.nm.config.NixDir, filename)); err == nil {
285  			if err := os.Remove(filepath.Join(np.nm.config.NixDir, filename)); err != nil {
286  				return fmt.Errorf("failed to remove file %s: %w", filename, err)
287  			}
288  		}
289  		return nil
290  	})
291  }
292  
293  func (np *nixPatch) writeDogeboxNixFile(filename string, content string) error {
294  	fullPath := filepath.Join(np.nm.config.NixDir, filename)
295  
296  	err := os.MkdirAll(filepath.Dir(fullPath), 0755)
297  	if err != nil {
298  		return fmt.Errorf("failed to create directories for %s: %w", fullPath, err)
299  	}
300  	err = os.WriteFile(fullPath, []byte(content), 0644)
301  	if err != nil {
302  		return fmt.Errorf("failed to write file %s: %w", fullPath, err)
303  	}
304  
305  	return nil
306  }
307  
308  func copyDirectory(srcDir, destDir string) error {
309  	err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
310  		if err != nil {
311  			return err
312  		}
313  
314  		relPath, err := filepath.Rel(srcDir, path)
315  		if err != nil {
316  			return err
317  		}
318  
319  		destPath := filepath.Join(destDir, relPath)
320  
321  		if info.IsDir() {
322  			return os.MkdirAll(destPath, info.Mode())
323  		}
324  
325  		srcFile, err := os.Open(path)
326  		if err != nil {
327  			return err
328  		}
329  		defer srcFile.Close()
330  
331  		destFile, err := os.Create(destPath)
332  		if err != nil {
333  			return err
334  		}
335  		defer destFile.Close()
336  
337  		_, err = io.Copy(destFile, srcFile)
338  		if err != nil {
339  			return err
340  		}
341  
342  		return os.Chmod(destPath, info.Mode())
343  	})
344  	if err != nil {
345  		return fmt.Errorf("failed to copy files: %w", err)
346  	}
347  
348  	return nil
349  }