diff --git a/apps/go.mod b/apps/go.mod index 81e1058..8228180 100644 --- a/apps/go.mod +++ b/apps/go.mod @@ -5,8 +5,9 @@ go 1.19 require ( github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 - github.com/hashicorp/nomad/api v0.0.0-20220909162634-8ff79d8a2da0 + github.com/hashicorp/nomad/api v0.0.0-20220930123803-fb1f5ea2d981 github.com/spf13/cobra v1.5.0 + golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/apps/go.sum b/apps/go.sum index 0798c05..b335d0f 100644 --- a/apps/go.sum +++ b/apps/go.sum @@ -1,6 +1,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -17,8 +17,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/nomad/api v0.0.0-20220909162634-8ff79d8a2da0 h1:gsbyjDOGAUIM9UJc0JknQ9XCr3m2AasG+eXoufoNgdA= -github.com/hashicorp/nomad/api v0.0.0-20220909162634-8ff79d8a2da0/go.mod h1:Z0U0rpbh4Qlkgqu3iRDcfJBA+r3FgoeD1BfigmZhfzM= +github.com/hashicorp/nomad/api v0.0.0-20220930123803-fb1f5ea2d981 h1:Kxz3mE0kP/ffUjRCvDCVNa8pjKEnZPKE4tDm5qnPWOY= +github.com/hashicorp/nomad/api v0.0.0-20220930123803-fb1f5ea2d981/go.mod h1:1dS8jZqAXhEreBcb26wpaV4Llk2cLO2sucuDKI+oTIs= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -32,12 +32,14 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shoenig/test v0.3.1 h1:dhGZztS6nQuvJ0o0RtUiQHaEO4hhArh/WmWwik3Ols0= +github.com/shoenig/test v0.4.0 h1:3X4xG/Chx7mzi0h71Sm6Vo38q0EYaQIBZpYFRcA1HVM= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 h1:lNtcVz/3bOstm7Vebox+5m3nLh/BYWnhmc3AhXOW6oI= +golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/apps/homadctl/cmd/containers.go b/apps/homadctl/cmd/containers.go new file mode 100644 index 0000000..394e315 --- /dev/null +++ b/apps/homadctl/cmd/containers.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/davidsbond/homad/apps/homadctl/internal/nomad" + "github.com/spf13/cobra" +) + +// Containers is the root command for tasks that manage container images within the homelab. +func Containers() *cobra.Command { + cmd := &cobra.Command{ + Use: "containers", + Aliases: []string{"container"}, + Args: cobra.NoArgs, + Short: "Subcommands for managing containers running in the homelab", + } + + cmd.AddCommand( + containersList(), + ) + + return cmd +} + +func containersList() *cobra.Command { + var jsonOut bool + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + Short: "List all containers running in the homelab", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := nomad.NewClient() + if err != nil { + return err + } + + images, err := nomad.Images(cmd.Context(), client) + if err != nil { + return err + } + + if jsonOut { + return json.NewEncoder(os.Stdout).Encode(images) + } + + for _, image := range images { + fmt.Println(image) + } + + return nil + }, + } + + flags := cmd.PersistentFlags() + flags.BoolVar(&jsonOut, "json", false, "Output containers in JSON format") + + return cmd +} diff --git a/apps/homadctl/cmd/gc.go b/apps/homadctl/cmd/gc.go index f412668..49c4127 100644 --- a/apps/homadctl/cmd/gc.go +++ b/apps/homadctl/cmd/gc.go @@ -1,57 +1,22 @@ package cmd import ( - "strings" - "github.com/davidsbond/homad/apps/homadctl/internal/nomad" - "github.com/hashicorp/nomad/api" "github.com/spf13/cobra" ) -// GC returns a cobra command that triggers all periodic jobs in the maintenance namespace that have a suffix of -// "gc". This is how garbage-collection tasks are typically named. +// GC returns a cobra command that triggers all periodic garbage collection jobs. func GC() *cobra.Command { return &cobra.Command{ Use: "gc", Short: "Trigger all garbage collection jobs available", RunE: func(cmd *cobra.Command, args []string) error { - const namespace = "maintenance" - client, err := nomad.NewClient() if err != nil { return err } - client.SetNamespace(namespace) - jobs, _, err := client.Jobs().List(&api.QueryOptions{}) - switch { - case err != nil: - return err - case len(jobs) == 0: - return nil - } - - jobIDs := make([]string, 0) - for _, job := range jobs { - if !strings.HasSuffix(job.Name, "gc") { - continue - } - - jobIDs = append(jobIDs, job.ID) - } - - if len(jobIDs) == 0 { - return nil - } - - for _, jobID := range jobIDs { - _, _, err = client.Jobs().PeriodicForce(jobID, &api.WriteOptions{}) - if err != nil { - return err - } - } - - return nil + return nomad.GarbageCollect(cmd.Context(), client) }, } } diff --git a/apps/homadctl/internal/nomad/client.go b/apps/homadctl/internal/nomad/client.go index 12fc124..276b4f0 100644 --- a/apps/homadctl/internal/nomad/client.go +++ b/apps/homadctl/internal/nomad/client.go @@ -1,11 +1,103 @@ // Package nomad provides functions for interacting with a nomad cluster. package nomad -import "github.com/hashicorp/nomad/api" +import ( + "context" + "net/http" + "strings" + "time" + + "github.com/hashicorp/nomad/api" + "golang.org/x/exp/maps" +) // NewClient returns a new nomad client configured to perform calls to the homelab. func NewClient() (*api.Client, error) { return api.NewClient(&api.Config{ - Address: "https://homelab.dsb.dev", + Address: "https://homelab.dsb.dev", + Namespace: api.AllNamespacesNamespace, + HttpClient: &http.Client{ + Timeout: time.Minute, + }, }) } + +// GarbageCollect triggers all periodic jobs within the nomad cluster that are suffixed with "gc". This is the typical +// pattern for custom garbage collection jobs. +func GarbageCollect(ctx context.Context, client *api.Client) error { + jobs, _, err := client.Jobs().List(&api.QueryOptions{Namespace: api.AllNamespacesNamespace}) + switch { + case err != nil: + return err + case len(jobs) == 0: + return nil + } + + for _, job := range jobs { + if ctx.Err() != nil { + return ctx.Err() + } + + if !strings.HasSuffix(job.Name, "gc") { + continue + } + + _, _, err = client.Jobs().PeriodicForce(job.ID, &api.WriteOptions{Namespace: job.Namespace}) + if err != nil { + return err + } + } + + return nil +} + +// Images returns a slice of all container image tags used by tasks within the nomad cluster. +func Images(ctx context.Context, client *api.Client) ([]string, error) { + images := make(map[string]struct{}) + + jobs, _, err := client.Jobs().List(&api.QueryOptions{Namespace: api.AllNamespacesNamespace}) + if err != nil { + return nil, err + } + + for _, job := range jobs { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + info, _, err := client.Jobs().Info(job.ID, &api.QueryOptions{Namespace: job.Namespace}) + if err != nil { + return nil, err + } + + for _, taskGroup := range info.TaskGroups { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + for _, task := range taskGroup.Tasks { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + if task.Driver != "docker" { + continue + } + + imageInterface, ok := task.Config["image"] + if !ok { + continue + } + + image, ok := imageInterface.(string) + if !ok { + continue + } + + images[image] = struct{}{} + } + } + } + + return maps.Keys(images), nil +} diff --git a/apps/homadctl/main.go b/apps/homadctl/main.go index 5884801..e155db0 100644 --- a/apps/homadctl/main.go +++ b/apps/homadctl/main.go @@ -27,6 +27,7 @@ func main() { root.AddCommand( cmd.GC(), + cmd.Containers(), ) if err := root.ExecuteContext(ctx); err != nil {