commit 91c16cf429d64321c477c63ef341a97f576237a9 Author: rony5394 <143897221+rony5394@users.noreply.github.com> Date: Sat Jan 31 21:54:14 2026 +0100 I am not proud of it. Why did I choose to do this. I could get some more sleep but I choose to write this. I just want some snapshot before I start breaking stuff. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2c77fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +ssh-key +ssh-key.pub diff --git a/config.json b/config.json new file mode 100644 index 0000000..1266d0b --- /dev/null +++ b/config.json @@ -0,0 +1,15 @@ +{ + "Nodes": { + "rpi":{ + "Ip": "192.168.1.175", + "DockerVolumePath": "/var/lib/docker/volumes" + }, + "arnost":{ + "Ip": "192.168.1.9", + "DockerVolumePath": "/home/blazena/volumes" + } + }, + "DockerManagerBaseUrl": "http://localhost:1234", + "User": "blazena", + "LocalBasePath": "/archive/@backups" +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..b5346ed --- /dev/null +++ b/config/config.go @@ -0,0 +1,11 @@ +package config; + +type Config struct { + Nodes map[string] struct{ + Ip string + DockerVolumePath string + } + DockerManagerBaseUrl string + User string + LocalBasePath string +}; diff --git a/docker/docker.go b/docker/docker.go new file mode 100644 index 0000000..af06108 --- /dev/null +++ b/docker/docker.go @@ -0,0 +1,128 @@ +package docker + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "net/http" + + "github.com/moby/moby/client" +) + +// Add mutex. +var ApiClient *client.Client; +var scale sync.Map; +var token string = "12345"; + +type aService struct{ + ServiceId string `json:"serviceId"`; + VolumeNames []string `json:"volumeNames"`; + Node string `json:"node"`; +} + +func Run(){ + ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM); + + var err error; + ApiClient, err = client.New(client.FromEnv); + if(err != nil){ + panic("Docker client was not able to init from env!" + err.Error()); + } + + info, err := ApiClient.Info(context.Background(), client.InfoOptions{}); + if(err != nil){ + panic("Error getting info!" + err.Error()); + } + + if(!info.Info.Swarm.ControlAvailable){ + panic("Node is not a swarm manager."); + } + + server := &http.Server{ + Addr: ":1234", + } + + http.HandleFunc("/services", listServices); + http.HandleFunc("/scale/down", scaleDown); + http.HandleFunc("/scale/up", scaleUp); + http.HandleFunc("/prepare", prepare); + go func(){ + err = server.ListenAndServe(); + if err == http.ErrServerClosed { + return; + + } + if(err != nil){ + panic("Unable to start http server!" + err.Error()); + } + + }(); + + time.Sleep(10*time.Millisecond); + <-ctx.Done(); + fmt.Println("Stopping http server."); + server.Close(); + fmt.Println("Exiting!"); +} + +func bearerAuth(w http.ResponseWriter, r *http.Request)bool { + authHeader := r.Header.Get("Authorization") + expected := "Bearer " + token + + if authHeader != expected { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintln(w, "Unauthorized") + return false; + } + return true; +} + +func listServices(w http.ResponseWriter, r *http.Request){ + if(r.Method != http.MethodGet){ + w.WriteHeader(http.StatusMethodNotAllowed); + return; + } + + if !bearerAuth(w,r) {return}; + + list, err := ApiClient.ServiceList(context.Background(), client.ServiceListOptions{}); + if(err != nil){ + panic("Unable to list services!" + err.Error()); + } + + var services []aService; + + for _, service:= range list.Items{ + var settings map[string]string = service.Spec.Labels; + + + if(settings["blazena.enable"] != "true"){ + continue; + + } + + targetVolumes := strings.Split(settings["blazena.volumes"], ","); + + services = append(services, aService{ + ServiceId: service.ID, + VolumeNames: targetVolumes, + Node: settings["blazena.node"], + }); + + } + + bytes, err := json.Marshal(services); + + if err != nil{ + panic("Error during response encoding!" + err.Error()); + } + + fmt.Fprint(w, string(bytes)); +} diff --git a/docker/prepare.go b/docker/prepare.go new file mode 100644 index 0000000..21f41e8 --- /dev/null +++ b/docker/prepare.go @@ -0,0 +1,69 @@ +package docker + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/api/types/mount" + "github.com/moby/moby/client" +) + +func prepare(w http.ResponseWriter, r *http.Request){ + if r.Method != http.MethodPost{ + w.WriteHeader(http.StatusMethodNotAllowed); + fmt.Fprint(w, "Method Not Allowed"); + return; + } + + //TODO: add token auth + + rawBody, err := io.ReadAll(r.Body); + if err != nil { + panic("Failed to read body!"); + } + + var bodyDecoded struct{ + volume string + node string + }; + + err = json.Unmarshal(rawBody, &bodyDecoded); + if err != nil { + panic("Failed to unmarshal json."+ err.Error()); + } + + maxConcurrent := uint64(1); + totalCompletions := uint64(1); + targetVolume := bodyDecoded.volume; + targetNode := bodyDecoded.node; + + ApiClient.ServiceCreate(context.Background(), client.ServiceCreateOptions{ + Spec: swarm.ServiceSpec{ + Mode: swarm.ServiceMode{ + ReplicatedJob: &swarm.ReplicatedJob{ + MaxConcurrent: &maxConcurrent, + TotalCompletions: &totalCompletions, + }, + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: &swarm.ContainerSpec{ + Image: "docker.io/library/alpine:latest", + Mounts: []mount.Mount{ + mount.Mount{ + Source: targetVolume, + Target: "/volume", + Type: "bind", + }, + }, + }, + Placement: &swarm.Placement{ + Constraints: []string{"node.hostname=="+targetNode}, + }, + }, + }, + }); +} diff --git a/docker/scale.go b/docker/scale.go new file mode 100644 index 0000000..42cb30e --- /dev/null +++ b/docker/scale.go @@ -0,0 +1,105 @@ +package docker; +import( + "net/http" + "io" + "encoding/json" + "context" + "github.com/moby/moby/client" +); + +func scaleDown(w http.ResponseWriter, r *http.Request){ + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed); + return; + } + + if !bearerAuth(w, r) {return} + + rawBody, err := io.ReadAll(r.Body); + if err != nil { + panic("Failed to read body!" + err.Error()); + } + + var parsedBody struct{ + ServiceId string `json:"serviceId"`; + }; + + err = json.Unmarshal(rawBody, &parsedBody); + if err != nil{ + panic("Failed to unmarshal request body!"+ err.Error()); + } + + inspectresoult, err := ApiClient.ServiceInspect(context.Background(), parsedBody.ServiceId, client.ServiceInspectOptions{}); + + if err != nil{ + panic("Error inspecting service!"+ err.Error()); + } + + originalScale := inspectresoult.Service.Spec.Mode.Replicated.Replicas; + updatedSpec := inspectresoult.Service.Spec; + + newScale := uint64(0); + updatedSpec.Mode.Replicated.Replicas = &newScale; + + scale.Store(parsedBody.ServiceId, *originalScale); + + _, err = ApiClient.ServiceUpdate(context.Background(), parsedBody.ServiceId, client.ServiceUpdateOptions{ + Spec: updatedSpec, + Version: inspectresoult.Service.Version, + }); + + if(err != nil){ + panic("Failed to update service."+ err.Error()); + } +} + +func scaleUp(w http.ResponseWriter, r *http.Request){ + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed); + return; + } + + if !bearerAuth(w, r) {return} + + + rawBody, err := io.ReadAll(r.Body); + if err != nil { + panic("Failed to read body!"); + } + + var parsedBody struct{ + ServiceId string `json:"serviceId"`; + }; + + err = json.Unmarshal(rawBody, &parsedBody); + if err != nil{ + panic("Failed to unmarshal request body!"+ err.Error()); + } + + inspectresoult, err := ApiClient.ServiceInspect(context.Background(), parsedBody.ServiceId, client.ServiceInspectOptions{}); + + if err != nil{ + panic("Error inspecting service!"+ err.Error()); + } + + originalScale, ok := scale.Load(parsedBody.ServiceId); + if(!ok){ + panic("Its not okay!"); + } + + originalScaleChecked, ok := originalScale.(uint64); + if(!ok){ + panic("Its very not okay!") + } + updatedSpec := inspectresoult.Service.Spec; + + updatedSpec.Mode.Replicated.Replicas = &originalScaleChecked; + + + ApiClient.ServiceUpdate(context.Background(), parsedBody.ServiceId, client.ServiceUpdateOptions{ + Spec: updatedSpec, + Version: inspectresoult.Service.Version, + + }); + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0370206 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module github.com/rony5394/blazena + +go 1.25.5 + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/moby/api v1.52.0 // indirect + github.com/moby/moby/client v0.2.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/sys v0.33.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f8bc319 --- /dev/null +++ b/go.sum @@ -0,0 +1,41 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg= +github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k= +github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/host/host.go b/host/host.go new file mode 100644 index 0000000..804bad2 --- /dev/null +++ b/host/host.go @@ -0,0 +1,63 @@ +package host + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + cfg "github.com/rony5394/blazena/config" +) + +var token string = "12345"; + +type aService struct{ + ServiceId string `json:"serviceId"`; + VolumeNames []string `json:"volumeNames"`; + Node string `json:"node"`; +} + +func Run(Config cfg.Config) { + services := getServices(Config); + + for _, service := range services { + _, ok := Config.Nodes[service.Node]; + if !ok { + fmt.Println("Node", service.Node, "refferenced in", service.ServiceId ,"service does not exists!"); + continue; + } + for _, volume := range service.VolumeNames{ + remoteFilePath := Config.Nodes[service.Node].DockerVolumePath + "/" + volume; + remotePath := Config.User + "@" + Config.Nodes[service.Node].Ip + ":" + remoteFilePath; + base := "rsync -avh --delete --progress -e 'ssh -i ./ssh-key' "; + localPath := Config.LocalBasePath + "/@current/@" + service.Node; + + fmt.Println(base + remotePath + " " + localPath); + + } + } +} + +func getServices(Config cfg.Config)[]aService{ + req, err := http.NewRequest("GET", Config.DockerManagerBaseUrl + "/services", nil); + if err != nil { + panic("Failed to create request."+ err.Error()); + } + + req.Header.Add("Authorization", "Bearer "+ token); + + res, err := http.DefaultClient.Do(req); + if err != nil { + panic("Failed to send request."+ err.Error()); + } + reader, err := io.ReadAll(res.Body); + if err != nil { + panic("Failed to decode response body."+err.Error()); + } + var services []aService; + err = json.Unmarshal(reader, &services); + if err != nil { + panic("Failed to unmarshal response."); + } + return services; +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..012e3c1 --- /dev/null +++ b/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "os" + + "github.com/rony5394/blazena/docker" + "github.com/rony5394/blazena/host" + cfg "github.com/rony5394/blazena/config" +); + +var Config cfg.Config = cfg.Config{ + Nodes: make(map[string]struct{Ip string; DockerVolumePath string}), +}; + + +func main() { + if(len(os.Args) < 2){ + panic("Usage: blazena "); + } + + rawConfig, err := os.ReadFile("./config.json"); + if err != nil{ + panic("Failed it load config file." + err.Error()); + } + + err = json.Unmarshal(rawConfig, &Config); + + if err != nil{ + panic("Failed to unmarshal config." + err.Error()) + } + + mode := os.Args[1]; + switch mode { + case "docker": + docker.Run(); + break; + case "host": + host.Run(Config); + break; + default: + panic("Invalid runtime mode!"); + } +}