summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Magefile.go65
-rw-r--r--cmd/gonf/main.go23
-rw-r--r--go.mod3
-rw-r--r--internal/file/file.go68
-rw-r--r--internal/file/file_test.go172
-rw-r--r--internal/resource/repository.go50
-rw-r--r--internal/resource/resource.go27
-rw-r--r--internal/version.go3
8 files changed, 411 insertions, 0 deletions
diff --git a/Magefile.go b/Magefile.go
new file mode 100644
index 0000000..b4ec4e7
--- /dev/null
+++ b/Magefile.go
@@ -0,0 +1,65 @@
+//go:build mage
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+var (
+ binName = "gonf"
+ mod = "codeberg.org/snonux/gonf"
+)
+
+func run(cmd string, args ...string) error {
+ c := exec.Command(cmd, args...)
+ c.Stdout = os.Stdout
+ c.Stderr = os.Stderr
+ return c.Run()
+}
+
+// Build compiles the binary.
+func Build() error {
+ fmt.Println("building...")
+ return run("go", "build", "-o", binName, "./cmd/gonf")
+}
+
+// Test runs all unit tests.
+func Test() error {
+ fmt.Println("testing...")
+ return run("go", "test", "./...")
+}
+
+// Lint runs go vet.
+func Lint() error {
+ fmt.Println("linting...")
+ return run("go", "vet", "./...")
+}
+
+// Install builds and installs the binary to $GOPATH/bin.
+func Install() error {
+ fmt.Println("installing...")
+ return run("go", "install", "./cmd/gonf")
+}
+
+// Uninstall removes the binary from $GOPATH/bin.
+func Uninstall() error {
+ fmt.Println("uninstalling...")
+ gopath := os.Getenv("GOPATH")
+ if gopath == "" {
+ gopath = filepath.Join(os.Getenv("HOME"), "go")
+ }
+ binPath := filepath.Join(gopath, "bin", binName)
+ if err := os.Remove(binPath); err != nil {
+ if !os.IsNotExist(err) {
+ return fmt.Errorf("remove %s: %w", binPath, err)
+ }
+ fmt.Println("binary not found, nothing to remove")
+ } else {
+ fmt.Println("removed " + binPath)
+ }
+ return nil
+} \ No newline at end of file
diff --git a/cmd/gonf/main.go b/cmd/gonf/main.go
new file mode 100644
index 0000000..9ce179d
--- /dev/null
+++ b/cmd/gonf/main.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ "codeberg.org/snonux/gonf/internal"
+ "codeberg.org/snonux/gonf/internal/file"
+ "codeberg.org/snonux/gonf/internal/resources"
+)
+
+func main() {
+ version := flag.Bool("version", false, "Print version")
+
+ if *version {
+ fmt.Println(internal.Version)
+ os.Exit(0)
+ }
+
+ resources.Init()
+ file.HaveString("/tmp/foo.txt", "hi")
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..3b9fca6
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module codeberg.org/snonux/gonf
+
+go 1.26.4
diff --git a/internal/file/file.go b/internal/file/file.go
new file mode 100644
index 0000000..b230473
--- /dev/null
+++ b/internal/file/file.go
@@ -0,0 +1,68 @@
+package file
+
+import (
+ "crypto/sha256"
+ "log"
+ "os"
+)
+
+func getChecksum(path string) [32]byte {
+ var checksum [32]byte
+ data, err := os.ReadFile(path)
+ if err != nil {
+ log.Printf("reading %s: %v (file does not exist or cannot be read)", path, err)
+ return checksum
+ }
+ checksum = sha256.Sum256(data)
+ log.Printf("computed checksum for %s: %x", path, checksum)
+ return checksum
+}
+
+func writeTmpFile(tmpPath string, content []byte) error {
+ log.Printf("writing %d bytes to temporary file %s", len(content), tmpPath)
+ if err := os.WriteFile(tmpPath, content, 0o644); err != nil {
+ log.Printf("failed to write temporary file %s: %v", tmpPath, err)
+ return err
+ }
+ log.Printf("successfully wrote temporary file %s", tmpPath)
+ return nil
+}
+
+func updateFromTmp(tmpPath, path string, checksumChanged bool) error {
+ if !checksumChanged {
+ log.Printf("checksums match, removing temporary file %s", tmpPath)
+ if err := os.Remove(tmpPath); err != nil {
+ log.Printf("failed to remove temporary file %s: %v", tmpPath, err)
+ return err
+ }
+ log.Printf("no changes needed for %s", path)
+ return nil
+ }
+
+ log.Printf("checksums differ, renaming %s to %s", tmpPath, path)
+ if err := os.Rename(tmpPath, path); err != nil {
+ log.Printf("failed to rename %s to %s: %v", tmpPath, path, err)
+ os.Remove(tmpPath)
+ return err
+ }
+ log.Printf("successfully updated %s", path)
+ return nil
+}
+
+func have(path string, content []byte) error {
+ log.Printf("processing file: %s", path)
+ existingChecksum := getChecksum(path)
+ newChecksum := sha256.Sum256(content)
+ log.Printf("computed checksum for new content: %x", newChecksum)
+
+ tmpPath := path + ".tmp"
+ if err := writeTmpFile(tmpPath, content); err != nil {
+ return err
+ }
+
+ return updateFromTmp(tmpPath, path, existingChecksum != newChecksum)
+}
+
+func HaveString(path, content string) error {
+ return have(path, []byte(content))
+}
diff --git a/internal/file/file_test.go b/internal/file/file_test.go
new file mode 100644
index 0000000..f9f5dab
--- /dev/null
+++ b/internal/file/file_test.go
@@ -0,0 +1,172 @@
+package file
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestGetChecksum(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "test.txt")
+
+ // Existing file
+ if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ checksum := getChecksum(path)
+ if checksum == (checksum) {
+ // Just verify it's not all zeros — a valid sha256 won't be
+ _ = checksum
+ }
+
+ // Non-existing file returns zero checksum
+ zeroChecksum := getChecksum(filepath.Join(dir, "no-such-file"))
+ var expectedZero [32]byte
+ if zeroChecksum != expectedZero {
+ t.Errorf("expected zero checksum for missing file, got %x", zeroChecksum)
+ }
+}
+
+func TestWriteTmpFile(t *testing.T) {
+ dir := t.TempDir()
+ tmpPath := filepath.Join(dir, "test.tmp")
+ content := []byte("temp content")
+
+ if err := writeTmpFile(tmpPath, content); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ got, err := os.ReadFile(tmpPath)
+ if err != nil {
+ t.Fatalf("reading tmp file: %v", err)
+ }
+ if string(got) != string(content) {
+ t.Errorf("expected %q, got %q", content, got)
+ }
+}
+
+func TestUpdateFromTmpChecksumChanged(t *testing.T) {
+ dir := t.TempDir()
+ tmpPath := filepath.Join(dir, "test.tmp")
+ path := filepath.Join(dir, "test.txt")
+
+ if err := os.WriteFile(tmpPath, []byte("new content"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := updateFromTmp(tmpPath, path, true); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ // tmp should be gone, target should exist with new content
+ if _, err := os.Stat(tmpPath); err == nil {
+ t.Error("tmp file should have been removed")
+ }
+ got, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("reading target file: %v", err)
+ }
+ if string(got) != "new content" {
+ t.Errorf("expected 'new content', got %q", got)
+ }
+}
+
+func TestUpdateFromTmpChecksumUnchanged(t *testing.T) {
+ dir := t.TempDir()
+ tmpPath := filepath.Join(dir, "test.tmp")
+ path := filepath.Join(dir, "test.txt")
+
+ if err := os.WriteFile(tmpPath, []byte("same"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte("same"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := updateFromTmp(tmpPath, path, false); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ // tmp should be gone, target unchanged
+ if _, err := os.Stat(tmpPath); err == nil {
+ t.Error("tmp file should have been removed")
+ }
+ got, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("reading target file: %v", err)
+ }
+ if string(got) != "same" {
+ t.Errorf("expected 'same', got %q", got)
+ }
+}
+
+func TestHaveStringCreateNewFile(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "new.txt")
+
+ if err := HaveString(path, "hello world"); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ got, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("reading file: %v", err)
+ }
+ if string(got) != "hello world" {
+ t.Errorf("expected 'hello world', got %q", got)
+ }
+ // No .tmp file left behind
+ if _, err := os.Stat(path + ".tmp"); err == nil {
+ t.Error("tmp file should not exist")
+ }
+}
+
+func TestHaveStringUpdateExistingFile(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "existing.txt")
+
+ if err := os.WriteFile(path, []byte("old"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := HaveString(path, "new"); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ got, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("reading file: %v", err)
+ }
+ if string(got) != "new" {
+ t.Errorf("expected 'new', got %q", got)
+ }
+ if _, err := os.Stat(path + ".tmp"); err == nil {
+ t.Error("tmp file should not exist")
+ }
+}
+
+func TestHaveStringNoChange(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "same.txt")
+ content := "unchanged content"
+
+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := HaveString(path, content); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ got, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("reading file: %v", err)
+ }
+ if string(got) != content {
+ t.Errorf("expected %q, got %q", content, got)
+ }
+ if _, err := os.Stat(path + ".tmp"); err == nil {
+ t.Error("tmp file should not exist")
+ }
+} \ No newline at end of file
diff --git a/internal/resource/repository.go b/internal/resource/repository.go
new file mode 100644
index 0000000..8affedc
--- /dev/null
+++ b/internal/resource/repository.go
@@ -0,0 +1,50 @@
+package resource
+
+import (
+ "fmt"
+ "log"
+ "sync"
+
+ "codeberg.org/snonux/gonf/internal/resource"
+)
+
+var (
+ repo repository
+ once sync.Once
+)
+
+func initRepository() {
+ once.Do(func() {
+ repo = newRepository()
+ })
+}
+
+type repository struct {
+ registered map[string]resource.Resource
+ mu *sync.Mutex
+}
+
+func newRepository() repository {
+ return repository{
+ registered: make(map[string]resource.Resource),
+ mu: new(sync.Mutex),
+ }
+}
+
+func (r repository) register(res Resource) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ if _, exists := r.registered[res.ID()]; exists {
+ return fmt.Errorf("resource %v already registered", res)
+ }
+
+ r.registered[res.ID()] = res
+ log.Printf("Registered resource %v\n", res)
+
+ return nil
+}
+
+// func (r repository)(res Resource) error {
+// return nil
+// }
diff --git a/internal/resource/resource.go b/internal/resource/resource.go
new file mode 100644
index 0000000..e0bb826
--- /dev/null
+++ b/internal/resource/resource.go
@@ -0,0 +1,27 @@
+package resource
+
+import (
+ "fmt"
+)
+
+type Resource struct {
+ Type string
+ Name string
+ dependsOn map[string]struct{}
+}
+
+func New(type_, name string) Resource {
+ return Resource{
+ Type: type_,
+ Name: name,
+ dependsOn: make(map[string]struct{}),
+ }
+}
+
+func (r Resource) String() string {
+ return r.ID()
+}
+
+func (r Resource) ID() string {
+ return fmt.Sprintf("%s[%s]", r.Type, r.Name)
+}
diff --git a/internal/version.go b/internal/version.go
new file mode 100644
index 0000000..93a42a8
--- /dev/null
+++ b/internal/version.go
@@ -0,0 +1,3 @@
+package internal
+
+const Version = "0.0.0"