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 }