feat: init + crude start command
This commit is contained in:
parent
fb4fb70c29
commit
6335743afd
5 changed files with 255 additions and 0 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -21,3 +21,5 @@
|
||||||
# Go workspace file
|
# Go workspace file
|
||||||
go.work
|
go.work
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
spud
|
||||||
|
|
18
go.mod
Normal file
18
go.mod
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module spud
|
||||||
|
|
||||||
|
go 1.22.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/goccy/go-yaml v1.11.3
|
||||||
|
github.com/spf13/cobra v1.8.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fatih/color v1.10.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
|
)
|
36
go.sum
Normal file
36
go.sum
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||||
|
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||||
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
|
github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I=
|
||||||
|
github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||||
|
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||||
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
92
main.go
Normal file
92
main.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VolumeDefinition struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VolumeConfiguration struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Container string `yaml:"container"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerDefinition struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Image string `yaml:"image"`
|
||||||
|
Volumes []VolumeConfiguration `yaml:"volumes"`
|
||||||
|
ExtraArgs []string `yaml:"extra-args"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceDefinition struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Volumes []VolumeDefinition `yaml:"volumes"`
|
||||||
|
Containers []ContainerDefinition `yaml:"containers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetServiceDefinitionFromFile(path string) ServiceDefinition {
|
||||||
|
var definition ServiceDefinition
|
||||||
|
|
||||||
|
defData, err := os.ReadFile(path)
|
||||||
|
|
||||||
|
// TODO: Bubble up error?
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not parse service configuration at %s: %s", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = yaml.Unmarshal(defData, &definition); err != nil {
|
||||||
|
log.Fatalf("Could not unpack service configuration: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return definition
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateService(definition ServiceDefinition) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
err = CreatePod(definition.Name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
knownVolumes := map[string]string{}
|
||||||
|
|
||||||
|
for _, volume := range definition.Volumes {
|
||||||
|
namespacedName := definition.Name + "_" + volume.Name
|
||||||
|
CreateVolume(namespacedName, true)
|
||||||
|
knownVolumes[volume.Name] = namespacedName
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, container := range definition.Containers {
|
||||||
|
if err = CreateContainer(container, knownVolumes, definition.Name); err != nil {
|
||||||
|
log.Fatalf("%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cli = &cobra.Command{
|
||||||
|
Use: "spud",
|
||||||
|
Short: "A not-entirely-terrible-way to manage self-hosted services.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = &cobra.Command{
|
||||||
|
Use: "start",
|
||||||
|
Short: "Creates or updates a service based on the provided definition.",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
pathProvided := args[0]
|
||||||
|
def := GetServiceDefinitionFromFile(pathProvided)
|
||||||
|
CreateService(def)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cli.AddCommand(start)
|
||||||
|
cli.Execute()
|
||||||
|
}
|
107
podman.go
Normal file
107
podman.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Creates a Podman volume of name `name` if it does not exist.
|
||||||
|
*
|
||||||
|
* If the volume exists, then behaviour depends on `existsOk`:
|
||||||
|
* - If `existsOk` is truthy, then the already-exists error is ignored and
|
||||||
|
* nothing is done;
|
||||||
|
* - Else, an error is returned.
|
||||||
|
*/
|
||||||
|
func CreateVolume(name string, existsOk bool) error {
|
||||||
|
args := []string{"volume", "create", name}
|
||||||
|
|
||||||
|
if existsOk {
|
||||||
|
args = append(args, "--ignore")
|
||||||
|
}
|
||||||
|
|
||||||
|
command := exec.Command("podman", args...)
|
||||||
|
|
||||||
|
if err := command.Run(); err != nil {
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("✅ Created volume \"%s\".", name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Creates a Podman pod to keep related containers together.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
func CreatePod(name string) error {
|
||||||
|
args := []string{"pod", "create", "--replace", name}
|
||||||
|
|
||||||
|
command := exec.Command("podman", args...)
|
||||||
|
|
||||||
|
if err := command.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("✅ Created pod \"%s\".", name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Creates individual containers.
|
||||||
|
*/
|
||||||
|
func CreateContainer(definition ContainerDefinition, knownVolumes map[string]string, service string) error {
|
||||||
|
namespacedContainerName := service + "_" + definition.Name
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"run",
|
||||||
|
"-d",
|
||||||
|
"--name",
|
||||||
|
namespacedContainerName,
|
||||||
|
"--pod",
|
||||||
|
service,
|
||||||
|
"--replace",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, volume := range definition.Volumes {
|
||||||
|
var host string
|
||||||
|
container := volume.Container
|
||||||
|
|
||||||
|
if volume.Name != "" {
|
||||||
|
namespacedName := service + "_" + volume.Name
|
||||||
|
host = namespacedName
|
||||||
|
} else if volume.Host != "" {
|
||||||
|
host = volume.Host
|
||||||
|
} else {
|
||||||
|
log.Fatal("Invalid volume source configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
arg := []string{"-v", host + ":" + container}
|
||||||
|
|
||||||
|
args = append(args, arg...)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, definition.Image)
|
||||||
|
|
||||||
|
for _, extra := range definition.ExtraArgs {
|
||||||
|
args = append(args, extra)
|
||||||
|
}
|
||||||
|
|
||||||
|
command := exec.Command("podman", args...)
|
||||||
|
|
||||||
|
if err := command.Start(); err != nil {
|
||||||
|
log.Fatal("Failed to start")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := command.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("✅ Started container \"%s\".", namespacedContainerName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue