commit 26fb88e9ebc4192da36411de66032158b424369d from: Oliver Lowe date: Wed Jan 12 09:18:00 2022 UTC Generate CRUD code using go generate Explanation for use is in the README commit - 82fc97ff8eb537c28bce19af5ffb697773bd200c commit + 26fb88e9ebc4192da36411de66032158b424369d blob - 78f3060f0579f6a1d8864e12180a78cf336c87ba blob + 0f4a86c710832cc1ac128db30560d8b871d6635a --- README.md +++ README.md @@ -2,6 +2,10 @@ package icinga provides a client to the Icinga2 HTTP A [![godocs.io](http://godocs.io/olowe.co/icinga?status.svg)](http://godocs.io/olowe.co/icinga) +Send any patches, questions or requests to the [mailing list][list] [~otl/icinga@lists.sr.ht](mailto:~otl/icinga@lists.sr.ht) 🙂 + +[list]: https://lists.sr.ht/~otl/icinga + ## Quick Start A Client manages interaction with an Icinga2 server. @@ -46,6 +50,62 @@ For more detail, see the [godocs][godocs]. [godocs]: https://godocs.io/olowe.co/icinga +## Development + +Some code is automatically generated. Ensure it's up-to-date before starting work: + + go generate + +Make some changes, then run the tests. No fancy test framework: + + go test + +Send any questions, patches, requests to the mailing list [~otl/icinga@lists.sr.ht](mailto:~otl/icinga@lists.sr.ht) + +### Testing + +Some tests dial an instance of Icinga2 running on the loopback address +and the standard Icinga2 port 5665 ("127.0.0.1:5665"). If this fails, +those tests are skipped. To run these tests, create the following API +user: + + object ApiUser "root" { + password = "icinga" + permissions = [ "*" ] + } + +Getting data from 127.0.0.1:5665 to an Icinga server is left as an +exercise to the reader! + +Personally, I run an Alpine Linux virtual machine using qemu. You +could also use the [official Icinga2 container image][image]. + +[image]: https://hub.docker.com/r/icinga/icinga2 + +### Code generation + +Source code for the basic lookup, create and delete operations of some +Icinga2 object types, such as Host and Service, are generated +automatically. + +The shell script crud.sh writes Go source code by reading a template +file and doing some text substitution. It loops through object types, +piping the template file crud.skel into the awk script crud.awk for +each. + +crud.sh writes code to the standard output by default: + + ./crud.sh + +If the flag `-o` is set, code will be written to the file +specified instead of to standard output: + + ./crud.sh -o crud.go + +Code generation is used because the functions are trivial and call the exact +same underlying methods on Client anyway. The only thing that differs is the type. +Perhaps when Go gets type parameters then this will go away? + ## Why Another Package? The [icinga2 terraform provider][tf] uses the package [github.com/lrsmith/go-icinga2-api/iapi][lrsmith]. blob - /dev/null blob + 57745a77e751377bfb9591ab85be6498a03c4c39 (mode 644) --- /dev/null +++ crud.awk @@ -0,0 +1,6 @@ +{ + gsub("TYPE", type) + gsub("LOWER", tolower(type)) + gsub("PLURAL", tolower(type) "s") + print +} blob - d65e59b24fb1e7f2a481c9205a661f34ad299e46 blob + c91758f751aced6fa19666b0f27ba3d014a2446a --- host.go +++ host.go @@ -1,9 +1,6 @@ package icinga -import ( - "encoding/json" - "fmt" -) +import "encoding/json" // Host represents a Host object. type Host struct { @@ -68,54 +65,3 @@ func (h Host) MarshalJSON() ([]byte, error) { } return json.Marshal(jhost) } - -// Hosts returns Hosts matching the filter expression filter. -// If no hosts match, error wraps ErrNoMatch. -// To fetch all hosts, set filter to the empty string (""). -func (c *Client) Hosts(filter string) ([]Host, error) { - objects, err := c.filterObjects("/objects/hosts", filter) - if err != nil { - return nil, fmt.Errorf("get hosts filter %q: %w", filter, err) - } - var hosts []Host - for _, o := range objects { - v, ok := o.(Host) - if !ok { - return nil, fmt.Errorf("get all hosts: %T in response", v) - } - hosts = append(hosts, v) - } - return hosts, nil -} - -// LookupHost returns the Host identified by name. If no Host is found, -// error wraps ErrNotExist. -func (c *Client) LookupHost(name string) (Host, error) { - obj, err := c.lookupObject("/objects/hosts/" + name) - if err != nil { - return Host{}, fmt.Errorf("lookup %s: %w", name, err) - } - v, ok := obj.(Host) - if !ok { - return Host{}, fmt.Errorf("lookup %s: result type %T is not host", name, obj) - } - return v, nil -} - -// CreateHost creates the Host host. -// The Name and CheckCommand fields of host must be non-zero. -func (c *Client) CreateHost(host Host) error { - if err := c.createObject(host); err != nil { - return fmt.Errorf("create host %s: %w", host.Name, err) - } - return nil -} - -// DeleteHost deletes the Host identified by name. -// If no Host is found, error wraps ErrNotExist. -func (c *Client) DeleteHost(name string) error { - if err := c.deleteObject("/objects/hosts/" + name); err != nil { - return fmt.Errorf("delete host %s: %w", name, err) - } - return nil -} blob - /dev/null blob + a11f3493120c3b4c35c45f48922a3837c1ebf953 (mode 644) --- /dev/null +++ crud.go @@ -0,0 +1,159 @@ +// Code generated by ./crud.sh ; DO NOT EDIT. + +package icinga + +import "fmt" + +// Hosts returns a slice of Host matching the filter expression filter. +// If no hosts match, error wraps ErrNoMatch. +// To fetch all host, set filter to the empty string (""). +func (c *Client) Hosts(filter string) ([]Host, error) { + objects, err := c.filterObjects("/objects/hosts", filter) + if err != nil { + return nil, fmt.Errorf("get hosts filter %q: %w", filter, err) + } + var hosts []Host + for _, o := range objects { + v, ok := o.(Host) + if !ok { + return nil, fmt.Errorf("get hosts filter %q: %T in response", filter, v) + } + hosts = append(hosts, v) + } + return hosts, nil +} + +// LookupHost returns the Host identified by name. If no Host is found, error +// wraps ErrNotExist. +func (c *Client) LookupHost(name string) (Host, error) { + obj, err := c.lookupObject("/objects/hosts/" + name) + if err != nil { + return Host{}, fmt.Errorf("lookup host %s: %w", name, err) + } + v, ok := obj.(Host) + if !ok { + return Host{}, fmt.Errorf("lookup host %s: result type %T is not Host", name, v) + } + return v, nil +} + +// CreateHost creates host. Some fields of host must be set for successful +// creation; see the type definition of Host for details. +func (c *Client) CreateHost(host Host) error { + if err := c.createObject(host); err != nil { + return fmt.Errorf("create host %s: %w", host.Name, err) + } + return nil +} + +// DeleteHost deletes the Host identified by name. +// If no Host is found, error wraps ErrNotExist. +func (c *Client) DeleteHost(name string) error { + if err := c.deleteObject("/objects/hosts/" + name); err != nil { + return fmt.Errorf("delete host %s: %w", name, err) + } + return nil +} + +// Services returns a slice of Service matching the filter expression filter. +// If no hosts match, error wraps ErrNoMatch. +// To fetch all service, set filter to the empty string (""). +func (c *Client) Services(filter string) ([]Service, error) { + objects, err := c.filterObjects("/objects/services", filter) + if err != nil { + return nil, fmt.Errorf("get services filter %q: %w", filter, err) + } + var services []Service + for _, o := range objects { + v, ok := o.(Service) + if !ok { + return nil, fmt.Errorf("get services filter %q: %T in response", filter, v) + } + services = append(services, v) + } + return services, nil +} + +// LookupService returns the Service identified by name. If no Service is found, error +// wraps ErrNotExist. +func (c *Client) LookupService(name string) (Service, error) { + obj, err := c.lookupObject("/objects/services/" + name) + if err != nil { + return Service{}, fmt.Errorf("lookup service %s: %w", name, err) + } + v, ok := obj.(Service) + if !ok { + return Service{}, fmt.Errorf("lookup service %s: result type %T is not Service", name, v) + } + return v, nil +} + +// CreateService creates service. Some fields of service must be set for successful +// creation; see the type definition of Service for details. +func (c *Client) CreateService(service Service) error { + if err := c.createObject(service); err != nil { + return fmt.Errorf("create service %s: %w", service.Name, err) + } + return nil +} + +// DeleteService deletes the Service identified by name. +// If no Service is found, error wraps ErrNotExist. +func (c *Client) DeleteService(name string) error { + if err := c.deleteObject("/objects/services/" + name); err != nil { + return fmt.Errorf("delete service %s: %w", name, err) + } + return nil +} + +// Users returns a slice of User matching the filter expression filter. +// If no hosts match, error wraps ErrNoMatch. +// To fetch all user, set filter to the empty string (""). +func (c *Client) Users(filter string) ([]User, error) { + objects, err := c.filterObjects("/objects/users", filter) + if err != nil { + return nil, fmt.Errorf("get users filter %q: %w", filter, err) + } + var users []User + for _, o := range objects { + v, ok := o.(User) + if !ok { + return nil, fmt.Errorf("get users filter %q: %T in response", filter, v) + } + users = append(users, v) + } + return users, nil +} + +// LookupUser returns the User identified by name. If no User is found, error +// wraps ErrNotExist. +func (c *Client) LookupUser(name string) (User, error) { + obj, err := c.lookupObject("/objects/users/" + name) + if err != nil { + return User{}, fmt.Errorf("lookup user %s: %w", name, err) + } + v, ok := obj.(User) + if !ok { + return User{}, fmt.Errorf("lookup user %s: result type %T is not User", name, v) + } + return v, nil +} + +// CreateUser creates user. Some fields of user must be set for successful +// creation; see the type definition of User for details. +func (c *Client) CreateUser(user User) error { + if err := c.createObject(user); err != nil { + return fmt.Errorf("create user %s: %w", user.Name, err) + } + return nil +} + +// DeleteUser deletes the User identified by name. +// If no User is found, error wraps ErrNotExist. +func (c *Client) DeleteUser(name string) error { + if err := c.deleteObject("/objects/users/" + name); err != nil { + return fmt.Errorf("delete user %s: %w", name, err) + } + return nil +} + blob - /dev/null blob + c2fa08026c533355acbe737f3b03804ce4f614cc (mode 755) --- /dev/null +++ crud.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +types="Host Service User" + +args=`getopt o: $*` +if test $? -ne 0 +then + echo "usage: lookupgen.sh [-o file]" + exit 2 +fi +set -- $args +while test "$#" -ne 0 +do + case "$1" + in + -o) + file="$2"; shift; shift;; + --) + shift; break;; + esac +done + +head="// Code generated by $0 $@; DO NOT EDIT. + +package icinga + +import \"fmt\" +" + +if test -n "$file" +then + echo "$head" > "$file" +else + echo "$head" +fi + +for t in $types +do + if test -n "$file" + then + awk -v "type=$t" -f crud.awk crud.skel >> "$file" + else + awk -v "type=$t" -f crud.awk crud.skel + fi +done blob - /dev/null blob + cf986a305b5ea0e52dc58d6de5e4a6469b80f18a (mode 644) --- /dev/null +++ crud.skel @@ -0,0 +1,51 @@ +// TYPEs returns a slice of TYPE matching the filter expression filter. +// If no hosts match, error wraps ErrNoMatch. +// To fetch all LOWER, set filter to the empty string (""). +func (c *Client) TYPEs(filter string) ([]TYPE, error) { + objects, err := c.filterObjects("/objects/PLURAL", filter) + if err != nil { + return nil, fmt.Errorf("get PLURAL filter %q: %w", filter, err) + } + var PLURAL []TYPE + for _, o := range objects { + v, ok := o.(TYPE) + if !ok { + return nil, fmt.Errorf("get PLURAL filter %q: %T in response", filter, v) + } + PLURAL = append(PLURAL, v) + } + return PLURAL, nil +} + +// LookupTYPE returns the TYPE identified by name. If no TYPE is found, error +// wraps ErrNotExist. +func (c *Client) LookupTYPE(name string) (TYPE, error) { + obj, err := c.lookupObject("/objects/PLURAL/" + name) + if err != nil { + return TYPE{}, fmt.Errorf("lookup LOWER %s: %w", name, err) + } + v, ok := obj.(TYPE) + if !ok { + return TYPE{}, fmt.Errorf("lookup LOWER %s: result type %T is not TYPE", name, v) + } + return v, nil +} + +// CreateTYPE creates LOWER. Some fields of LOWER must be set for successful +// creation; see the type definition of TYPE for details. +func (c *Client) CreateTYPE(LOWER TYPE) error { + if err := c.createObject(LOWER); err != nil { + return fmt.Errorf("create LOWER %s: %w", LOWER.Name, err) + } + return nil +} + +// DeleteTYPE deletes the TYPE identified by name. +// If no TYPE is found, error wraps ErrNotExist. +func (c *Client) DeleteTYPE(name string) error { + if err := c.deleteObject("/objects/PLURAL/" + name); err != nil { + return fmt.Errorf("delete LOWER %s: %w", name, err) + } + return nil +} + blob - a09a08269b2f988f03f4099e057e222fde2177ac blob + 72cb8f1953b6a3cdff843006531f78c4a0895fa8 --- object.go +++ object.go @@ -1,3 +1,4 @@ + package icinga import ( @@ -11,10 +12,11 @@ import ( type object interface { name() string - attrs() map[string]interface{} path() string } +//go:generate ./crud.sh -o crud.go + func (c *Client) lookupObject(objpath string) (object, error) { resp, err := c.get(objpath, "") if err != nil { blob - 8d8a80f0fe3ffcf01b3a2054aaf32e0b6a8dccbf blob + 57ff914a3e5b3f142c17ae0e797b260301b9233d --- service.go +++ service.go @@ -1,9 +1,6 @@ package icinga -import ( - "encoding/json" - "fmt" -) +import "encoding/json" func (s Service) name() string { return s.Name @@ -63,22 +60,3 @@ func (s Service) MarshalJSON() ([]byte, error) { }{Attrs: attrs} return json.Marshal(jservice) } - -func (c *Client) CreateService(service Service) error { - if err := c.createObject(service); err != nil { - return fmt.Errorf("create service %s: %w", service.Name, err) - } - return nil -} - -func (c *Client) LookupService(name string) (Service, error) { - obj, err := c.lookupObject("/objects/services/" + name) - if err != nil { - return Service{}, fmt.Errorf("lookup %s: %w", name, err) - } - v, ok := obj.(Service) - if !ok { - return Service{}, fmt.Errorf("lookup %s: result type %T is not service", name, obj) - } - return v, nil -} blob - 1ad4fa1f9efbf7d447b2dbac6ea29dbda6992300 blob + b9c85caf88e41b5f1c13adf09e932558615f949f --- user.go +++ user.go @@ -1,9 +1,6 @@ package icinga -import ( - "encoding/json" - "fmt" -) +import "encoding/json" // User represents a User object. // Note that this is different from an ApiUser. @@ -47,49 +44,3 @@ func (u User) attrs() map[string]interface{} { m["email"] = u.Email return m } - -func (c *Client) Users() ([]User, error) { - objects, err := c.filterObjects("/objects/users", "") - if err != nil { - return nil, fmt.Errorf("get all users: %w", err) - } - var users []User - for _, o := range objects { - v, ok := o.(User) - if !ok { - return nil, fmt.Errorf("get all users: %T in response", v) - } - users = append(users, v) - } - return users, nil -} - -func (c *Client) LookupUser(name string) (User, error) { - obj, err := c.lookupObject("/objects/users/" + name) - if err != nil { - return User{}, fmt.Errorf("lookup %s: %w", name, err) - } - v, ok := obj.(User) - if !ok { - return User{}, fmt.Errorf("lookup %s: result type %T is not user", name, v) - } - return v, nil -} - -// CreateUser creates user. -// An error is returned if the User already exists or on any other error. -func (c *Client) CreateUser(user User) error { - if err := c.createObject(user); err != nil { - return fmt.Errorf("create user %s: %w", user.Name, err) - } - return nil -} - -// DeleteUser deletes the User identified by name. -// ErrNotExist is returned if the User doesn't exist. -func (c *Client) DeleteUser(name string) error { - if err := c.deleteObject("/objects/users/" + name); err != nil { - return fmt.Errorf("delete user %s: %w", name, err) - } - return nil -}