diff --git a/cmd/pkiadm/certificate.go b/cmd/pkiadm/certificate.go index a7c5836..0d1e9d7 100644 --- a/cmd/pkiadm/certificate.go +++ b/cmd/pkiadm/certificate.go @@ -88,9 +88,9 @@ func listCertificate(args []string, client *pkiadm.Client) error { return nil } out := tabwriter.NewWriter(os.Stdout, 2, 2, 1, ' ', tabwriter.AlignRight) - fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n", "id", "private", "csr", "ca", "serial", "created", "duration", "self-signed") + fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n", "id", "private", "csr", "ca", "serial", "created", "duration", "self-signed") for _, cert := range certs { - fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\t%t\t\n", cert.ID, cert.PrivateKey.ID, cert.CSR.ID, cert.CA.ID, cert.Serial.ID, cert.Created, cert.Duration, cert.IsCA) + fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%t\t\n", cert.ID, cert.PrivateKey.ID, cert.CSR.ID, cert.CA.ID, cert.Serial.ID, cert.Created, cert.Duration, cert.IsCA) } out.Flush() diff --git a/cmd/pkiadm/main.go b/cmd/pkiadm/main.go index 4138992..369ddf5 100644 --- a/cmd/pkiadm/main.go +++ b/cmd/pkiadm/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "sort" "text/tabwriter" "github.com/gibheer/pkiadm" @@ -185,6 +186,7 @@ func list(args []string, c *pkiadm.Client) error { if err != nil { return err } + sort.Sort(resources) out := tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', 0) fmt.Fprintf(out, "%s\t%s\t\n", "type", "id") for _, res := range resources { diff --git a/cmd/pkiadmd/ca.go b/cmd/pkiadmd/ca.go index db6b8d5..22818fa 100644 --- a/cmd/pkiadmd/ca.go +++ b/cmd/pkiadmd/ca.go @@ -22,6 +22,7 @@ type ( ID string Type pkiadm.CAType Certificate pkiadm.ResourceName + Interval Interval } ) @@ -87,12 +88,18 @@ func (ca *CA) Name() pkiadm.ResourceName { return pkiadm.ResourceName{ca.ID, pkiadm.RTCA} } -// AddDependency registers a depending resource to be retuened by Dependencies() -// Refresh must trigger a rebuild of the resource. +// Refresh must trigger a rebuild of the resource. In this case, this is a NOOP. func (ca *CA) Refresh(*Storage) error { return nil } +// RefreshInterval returns the dates and interval settings which are used to +// decide when to trigger a refresh for the resource. +// For CAs, this is a NOOP, as the underlying cert needs the refresh. +func (ca *CA) RefreshInterval() Interval { + return NoInterval +} + // Return the PEM output of the contained resource. func (ca *CA) Pem() ([]byte, error) { return []byte{}, nil } func (ca *CA) Checksum() []byte { return []byte{} } diff --git a/cmd/pkiadmd/certificate.go b/cmd/pkiadmd/certificate.go index db3c400..0f8830f 100644 --- a/cmd/pkiadmd/certificate.go +++ b/cmd/pkiadmd/certificate.go @@ -18,6 +18,8 @@ type ( ID string IsCA bool + Interval Interval + // TODO remove obsolete field - got replaced with interval Duration time.Duration Created time.Time @@ -31,6 +33,11 @@ type ( ) func NewCertificate(id string, privateKey, serial, csr, ca pkiadm.ResourceName, selfSign bool, duration time.Duration) (*Certificate, error) { + + if id == "" { + return nil, ENoIDGiven + } + return &Certificate{ ID: id, PrivateKey: privateKey, @@ -39,6 +46,10 @@ func NewCertificate(id string, privateKey, serial, csr, ca pkiadm.ResourceName, CA: ca, IsCA: selfSign, Duration: duration, + Interval: Interval{ + Created: time.Now(), + RefreshAfter: duration, + }, }, nil } @@ -86,10 +97,16 @@ func (c *Certificate) Refresh(lookup *Storage) error { return err } c.Data = pem.EncodeToMemory(&block) + // TODO remove obsolete field c.Created = time.Now() + c.Interval.LastRefresh = time.Now() return nil } +func (c *Certificate) RefreshInterval() Interval { + return c.Interval +} + func (c *Certificate) GetCertificate() (*pki.Certificate, error) { // TODO fix this, we must check if there is anything else block, _ := pem.Decode(c.Data) @@ -156,6 +173,7 @@ func (s *Server) SetCertificate(changeset pkiadm.CertificateChange, res *pkiadm. switch field { case "duration": cert.Duration = change.Duration + cert.Interval.RefreshAfter = change.Duration case "private": cert.PrivateKey = change.PrivateKey case "csr": diff --git a/cmd/pkiadmd/csr.go b/cmd/pkiadmd/csr.go index 9320f42..3700872 100644 --- a/cmd/pkiadmd/csr.go +++ b/cmd/pkiadmd/csr.go @@ -4,6 +4,7 @@ import ( "encoding/pem" "fmt" "net" + "time" "github.com/gibheer/pki" "github.com/gibheer/pkiadm" @@ -14,6 +15,9 @@ type ( // ID is the unique identifier of the CSR. ID string + // Interval represents the refresh timing information. + Interval Interval + // The following options are used to generate the content of the CSR. DNSNames []string EmailAddresses []string @@ -30,7 +34,8 @@ type ( // NewCSR creates a new CSR. func NewCSR(id string, pk, subject pkiadm.ResourceName, dnsNames []string, - emailAddresses []string, iPAddresses []net.IP) (*CSR, error) { + emailAddresses []string, iPAddresses []net.IP, refreshAfter time.Duration, + invalidAfter time.Duration) (*CSR, error) { return &CSR{ ID: id, Subject: subject, @@ -38,6 +43,11 @@ func NewCSR(id string, pk, subject pkiadm.ResourceName, dnsNames []string, EmailAddresses: emailAddresses, IPAddresses: iPAddresses, PrivateKey: pk, + Interval: Interval{ + Created: time.Now(), + RefreshAfter: refreshAfter, + InvalidAfter: invalidAfter, + }, }, nil } @@ -78,9 +88,14 @@ func (c *CSR) Refresh(lookup *Storage) error { return err } c.Data = pem.EncodeToMemory(&block) + c.Interval.LastRefresh = time.Now() return nil } +func (c *CSR) RefreshInterval() Interval { + return c.Interval +} + // Return the PEM output of the contained resource. func (c *CSR) Pem() ([]byte, error) { return c.Data, nil } func (c *CSR) Checksum() []byte { return Hash(c.Data) } @@ -111,6 +126,7 @@ func (s *Server) CreateCSR(inCSR pkiadm.CSR, res *pkiadm.Result) error { inCSR.DNSNames, inCSR.EmailAddresses, inCSR.IPAddresses, + 0, 0, ) if err != nil { res.SetError(err, "Could not create new private key '%s'", inCSR.ID) diff --git a/cmd/pkiadmd/location.go b/cmd/pkiadmd/location.go index f547826..25d4b26 100644 --- a/cmd/pkiadmd/location.go +++ b/cmd/pkiadmd/location.go @@ -6,6 +6,7 @@ import ( "log" "os" "os/exec" + "time" "github.com/gibheer/pkiadm" ) @@ -23,10 +24,13 @@ type ( Path string Dependencies []pkiadm.ResourceName + + Interval Interval } ) -func NewLocation(id, path, preCom, postCom string, res []pkiadm.ResourceName) (*Location, error) { +func NewLocation(id, path, preCom, postCom string, res []pkiadm.ResourceName, + interval Interval) (*Location, error) { if id == "" { return nil, ENoIDGiven } @@ -37,6 +41,7 @@ func NewLocation(id, path, preCom, postCom string, res []pkiadm.ResourceName) (* ID: id, Path: path, Dependencies: res, + Interval: interval, } return l, nil } @@ -78,9 +83,14 @@ func (l *Location) Refresh(lookup *Storage) error { return err } } + l.Interval.LastRefresh = time.Now() return nil } +func (l *Location) RefreshInterval() Interval { + return l.Interval +} + func (l *Location) DependsOn() []pkiadm.ResourceName { return l.Dependencies } // Pem is not used by location, as it does not contain any data. @@ -97,7 +107,7 @@ func (s *Server) CreateLocation(inLoc pkiadm.Location, res *pkiadm.Result) error for _, dep := range inLoc.Dependencies { deps = append(deps, pkiadm.ResourceName{ID: dep.ID, Type: dep.Type}) } - loc, err := NewLocation(inLoc.ID, inLoc.Path, inLoc.PreCommand, inLoc.PostCommand, deps) + loc, err := NewLocation(inLoc.ID, inLoc.Path, inLoc.PreCommand, inLoc.PostCommand, deps, NoInterval) if err != nil { res.SetError(err, "Could not create location '%s'", inLoc.ID) return nil diff --git a/cmd/pkiadmd/main.go b/cmd/pkiadmd/main.go index cabcead..d167c5c 100644 --- a/cmd/pkiadmd/main.go +++ b/cmd/pkiadmd/main.go @@ -6,6 +6,7 @@ import ( "net/rpc" "os" "os/signal" + "time" "github.com/gibheer/pkiadm" ) @@ -19,20 +20,42 @@ const ( EAlreadyExist = Error("resource already exists") ) +var ( + NoInterval = Interval{} +) + type ( Resource interface { // Return the unique ResourceName Name() pkiadm.ResourceName - // AddDependency registers a depending resource to be retuened by Dependencies() // Refresh must trigger a rebuild of the resource. Refresh(*Storage) error + // RefreshInterval returns the dates and interval settings which are used to + // decide when to trigger a refresh for the resource. + RefreshInterval() Interval // Return the PEM output of the contained resource. Pem() ([]byte, error) + // Return the checksum of the PEM content. Checksum() []byte // DependsOn must return the resource names it is depending on. DependsOn() []pkiadm.ResourceName } + Interval struct { + // Created states the time, the resource was created. + Created time.Time + // LastRefresh is the time, when the resource was last refreshed. + LastRefresh time.Time + // RefreshAfter is the duration after which the refresh of the resource + // is triggered. + RefreshAfter time.Duration + // InvalidAfter is the duration after which this resource becomes invalid. + // The decision when a resource becomes invalid is based on the created time + // and the duration. When the refresh duration is less than the invalid + // duration, then the resource will never be invalid. + InvalidAfter time.Duration + } + Error string ) diff --git a/cmd/pkiadmd/private_key.go b/cmd/pkiadmd/private_key.go index 51041ce..2bf2605 100644 --- a/cmd/pkiadmd/private_key.go +++ b/cmd/pkiadmd/private_key.go @@ -4,6 +4,7 @@ import ( "crypto/elliptic" "encoding/pem" "fmt" + "time" "github.com/gibheer/pki" "github.com/gibheer/pkiadm" @@ -17,14 +18,15 @@ const ( type ( PrivateKey struct { - ID string - PKType pkiadm.PrivateKeyType - Bits uint - Key []byte + ID string + PKType pkiadm.PrivateKeyType + Bits uint + Key []byte + Interval Interval } ) -func NewPrivateKey(id string, pkType pkiadm.PrivateKeyType, bits uint) (*PrivateKey, error) { +func NewPrivateKey(id string, pkType pkiadm.PrivateKeyType, bits uint, interval Interval) (*PrivateKey, error) { if id == "" { return nil, ENoIDGiven } @@ -32,9 +34,10 @@ func NewPrivateKey(id string, pkType pkiadm.PrivateKeyType, bits uint) (*Private return nil, err } pk := PrivateKey{ - ID: id, - PKType: pkType, - Bits: bits, + ID: id, + PKType: pkType, + Bits: bits, + Interval: interval, } return &pk, nil } @@ -88,9 +91,16 @@ func (p *PrivateKey) Refresh(_ *Storage) error { return err } p.Key = pem.EncodeToMemory(&block) + p.Interval.LastRefresh = time.Now() return nil } +// RefreshInterval returns the dates and interval settings which are used to +// decide when to trigger a refresh for the resource. +func (p *PrivateKey) RefreshInterval() Interval { + return p.Interval +} + func (p *PrivateKey) GetKey() (pki.PrivateKey, error) { var ( err error @@ -139,7 +149,7 @@ func (s *Server) CreatePrivateKey(inPk pkiadm.PrivateKey, res *pkiadm.Result) er s.lock() defer s.unlock() - pk, err := NewPrivateKey(inPk.ID, inPk.Type, inPk.Bits) + pk, err := NewPrivateKey(inPk.ID, inPk.Type, inPk.Bits, NoInterval) if err != nil { res.SetError(err, "Could not create new private key '%s'", inPk.ID) return nil diff --git a/cmd/pkiadmd/public_key.go b/cmd/pkiadmd/public_key.go index 7e91333..00ddad2 100644 --- a/cmd/pkiadmd/public_key.go +++ b/cmd/pkiadmd/public_key.go @@ -3,6 +3,7 @@ package main import ( "encoding/pem" "fmt" + "time" "github.com/gibheer/pkiadm" ) @@ -14,13 +15,21 @@ type ( PrivateKey pkiadm.ResourceName Type pkiadm.PrivateKeyType // mark the type of the public key Key []byte + + Interval Interval } ) -func NewPublicKey(id string, pk pkiadm.ResourceName) (*PublicKey, error) { +func NewPublicKey(id string, pk pkiadm.ResourceName, refreshAfter time.Duration, + invalidAfter time.Duration) (*PublicKey, error) { pub := PublicKey{ ID: id, PrivateKey: pk, + Interval: Interval{ + Created: time.Now(), + RefreshAfter: refreshAfter, + InvalidAfter: invalidAfter, + }, } return &pub, nil } @@ -45,9 +54,14 @@ func (p *PublicKey) Refresh(lookup *Storage) error { return err } p.Key = pem.EncodeToMemory(&block) + p.Interval.LastRefresh = time.Now() return nil } +func (p *PublicKey) RefreshInterval() Interval { + return p.Interval +} + func (p *PublicKey) DependsOn() []pkiadm.ResourceName { return []pkiadm.ResourceName{p.PrivateKey} } @@ -64,7 +78,7 @@ func (s *Server) CreatePublicKey(inPub pkiadm.PublicKey, res *pkiadm.Result) err s.lock() defer s.unlock() - pub, err := NewPublicKey(inPub.ID, inPub.PrivateKey) + pub, err := NewPublicKey(inPub.ID, inPub.PrivateKey, 0, 0) if err != nil { res.SetError(err, "Could not create public key '%s'", inPub.ID) return nil diff --git a/cmd/pkiadmd/serial.go b/cmd/pkiadmd/serial.go index 296f985..31d7548 100644 --- a/cmd/pkiadmd/serial.go +++ b/cmd/pkiadmd/serial.go @@ -33,7 +33,6 @@ func NewSerial(id string, min, max int64) (*Serial, error) { // Return the unique ResourceName func (s *Serial) Name() pkiadm.ResourceName { return pkiadm.ResourceName{s.ID, pkiadm.RTSerial} } -// AddDependency registers a depending resource to be retuened by Dependencies() // Refresh must trigger a rebuild of the resource. func (s *Serial) Refresh(*Storage) error { // This is a NOOP, because there is nothing to refresh. Depending resources @@ -41,6 +40,11 @@ func (s *Serial) Refresh(*Storage) error { return nil } +// RefreshInterval is a NOOP here, as serials can't be refreshed. +func (s *Serial) RefreshInterval() Interval { + return NoInterval +} + // Return the PEM output of the contained resource. func (s *Serial) Pem() ([]byte, error) { return []byte{}, nil } func (s *Serial) Checksum() []byte { return []byte{} } diff --git a/cmd/pkiadmd/server.go b/cmd/pkiadmd/server.go index b766031..0cd45ad 100644 --- a/cmd/pkiadmd/server.go +++ b/cmd/pkiadmd/server.go @@ -33,26 +33,10 @@ func (s *Server) store(res *pkiadm.Result) error { } func (s *Server) List(filter pkiadm.Filter, result *pkiadm.ResultResource) error { - for _, res := range s.storage.PrivateKeys { - result.Resources = append(result.Resources, res.Name()) - } - for _, res := range s.storage.PublicKeys { - result.Resources = append(result.Resources, res.Name()) - } - for _, res := range s.storage.Locations { - result.Resources = append(result.Resources, res.Name()) - } - for _, res := range s.storage.Certificates { - result.Resources = append(result.Resources, res.Name()) - } - for _, res := range s.storage.CSRs { - result.Resources = append(result.Resources, res.Name()) - } - for _, res := range s.storage.Serials { - result.Resources = append(result.Resources, res.Name()) - } - for _, res := range s.storage.Subjects { - result.Resources = append(result.Resources, res.Name()) + resources := s.storage.List() + result.Resources = make([]pkiadm.ResourceName, len(resources)) + for i, res := range resources { + result.Resources[i] = res.Name() } return nil } diff --git a/cmd/pkiadmd/storage.go b/cmd/pkiadmd/storage.go index c8cb6fb..9cca7f5 100644 --- a/cmd/pkiadmd/storage.go +++ b/cmd/pkiadmd/storage.go @@ -6,6 +6,8 @@ import ( "io/ioutil" "log" "os" + "sort" + "time" "github.com/gibheer/pkiadm" "github.com/pkg/errors" @@ -28,6 +30,19 @@ type ( // dependencies maps from a resource name to all resources which depend // on it. dependencies map[string]map[string]Resource + // refresh order contains all resources in the order they need to be + // refreshed next. + refreshOrder RefreshList + refreshTimer *time.Timer + } + + // RefreshList is a list of resources + RefreshList []RefreshSet + // RefreshSet contains the vital information to decide, when to refresh + // the specified resource. + RefreshSet struct { + Name pkiadm.ResourceName + Interval Interval } ) @@ -68,6 +83,7 @@ func (s *Storage) load() error { if err := s.refreshDependencies(); err != nil { return err } + s.scanForRefresh() return nil } @@ -101,6 +117,69 @@ func (s *Storage) refreshDependencies() error { return nil } +// scanForRefresh updates the list of resources that need an update. +func (s *Storage) scanForRefresh() { + if s.refreshTimer != nil { + s.refreshTimer.Stop() + } + refList := RefreshList{} + for _, res := range s.PrivateKeys { + refList.Add(res) + } + for _, res := range s.PublicKeys { + refList.Add(res) + } + for _, res := range s.CSRs { + refList.Add(res) + } + for _, res := range s.Certificates { + refList.Add(res) + } + for _, res := range s.Locations { + refList.Add(res) + } + sort.Sort(refList) + if len(refList) == 0 { + log.Println("nothing found to refresh, looking again in 24h") + s.refreshTimer = time.AfterFunc(24*time.Hour, s.scanForRefresh) + return + } + s.refreshOrder = refList + duration := refList[0].Interval.LastRefresh. + Add(refList[0].Interval.RefreshAfter). + Sub(time.Now()) + if duration <= 5*time.Second { + duration = 5 * time.Second + } + log.Printf("next refresh planned for '%s' in %s", refList[0].Name, duration) + s.refreshTimer = time.AfterFunc( + duration, + s.refresh, + ) +} + +func (s *Storage) refresh() { + if len(s.refreshOrder) == 0 { + return + } + resName := s.refreshOrder[0].Name + res, err := s.Get(resName) + if err != nil { + // the resource doesn't exist anymore, so just rescan + log.Printf("resource to refresh has gone away: %s", resName) + goto rescan + } + if err := res.Refresh(s); err != nil { + log.Printf("error refreshing resource '%s': %s", res.Name(), err) + } + if err := s.store(); err != nil { + log.Printf("could not update resources: %s", err) + } +rescan: + log.Printf("rescanning for new entries") + s.scanForRefresh() +} + // addDependency adds a resource to the dependency graph. func (s *Storage) addDependency(r Resource) error { for _, rn := range r.DependsOn() { @@ -138,6 +217,7 @@ func (s *Storage) AddSerial(se *Serial) error { return err } s.Serials[se.Name().ID] = se + s.scanForRefresh() return s.addDependency(se) } @@ -150,6 +230,7 @@ func (s *Storage) AddSubject(se *Subject) error { return err } s.Subjects[se.Name().ID] = se + s.scanForRefresh() return s.addDependency(se) } @@ -159,6 +240,7 @@ func (s *Storage) AddPrivateKey(pk *PrivateKey) error { return err } s.PrivateKeys[pk.Name().ID] = pk + s.scanForRefresh() return s.addDependency(pk) } @@ -168,6 +250,7 @@ func (s *Storage) AddPublicKey(pub *PublicKey) error { return err } s.PublicKeys[pub.Name().ID] = pub + s.scanForRefresh() return s.addDependency(pub) } @@ -177,6 +260,7 @@ func (s *Storage) AddCertificate(cert *Certificate) error { return err } s.Certificates[cert.Name().ID] = cert + s.scanForRefresh() return s.addDependency(cert) } @@ -186,6 +270,7 @@ func (s *Storage) AddCSR(csr *CSR) error { return err } s.CSRs[csr.Name().ID] = csr + s.scanForRefresh() return s.addDependency(csr) } @@ -195,6 +280,7 @@ func (s *Storage) AddLocation(l *Location) error { return err } s.Locations[l.Name().ID] = l + s.scanForRefresh() return s.addDependency(l) } @@ -203,6 +289,7 @@ func (s *Storage) AddCA(ca *CA) error { return err } s.CAs[ca.Name().ID] = ca + s.scanForRefresh() return s.addDependency(ca) } @@ -326,6 +413,7 @@ func (s *Storage) Remove(r Resource) error { delete(deps, r.Name().String()) } } + s.scanForRefresh() return nil } @@ -364,5 +452,65 @@ func (s *Storage) Update(rn pkiadm.ResourceName) error { return err } } + s.scanForRefresh() return nil } + +// List returns all currently registered resources. +func (s *Storage) List() []Resource { + resources := []Resource{} + for _, res := range s.PrivateKeys { + resources = append(resources, res) + } + for _, res := range s.PublicKeys { + resources = append(resources, res) + } + for _, res := range s.Locations { + resources = append(resources, res) + } + for _, res := range s.Certificates { + resources = append(resources, res) + } + for _, res := range s.CSRs { + resources = append(resources, res) + } + for _, res := range s.Serials { + resources = append(resources, res) + } + for _, res := range s.Subjects { + resources = append(resources, res) + } + return resources +} + +// Add adds a resource to the refreshList when it should be refreshed. +func (refList *RefreshList) Add(res Resource) { + refSet := RefreshSet{ + Name: res.Name(), + Interval: res.RefreshInterval(), + } + if refSet.Interval.RefreshAfter <= 0 { + return + } + newRefList := append(*refList, refSet) + *refList = newRefList +} + +// Len is the number of elements in the collection. +func (refList RefreshList) Len() int { return len(refList) } + +// Less reports whether the element with +// index i should sort before the element with index j. +func (refList RefreshList) Less(i, j int) bool { + return 0 > refList[i].Interval.LastRefresh.Add( + refList[i].Interval.RefreshAfter, + ).Sub( + refList[j].Interval.LastRefresh.Add( + refList[j].Interval.RefreshAfter), + ) +} + +// Swap swaps the elements with indexes i and j. +func (refList RefreshList) Swap(i, j int) { + refList[i], refList[j] = refList[j], refList[i] +} diff --git a/cmd/pkiadmd/subject.go b/cmd/pkiadmd/subject.go index 6f5aa05..3489bff 100644 --- a/cmd/pkiadmd/subject.go +++ b/cmd/pkiadmd/subject.go @@ -3,14 +3,16 @@ package main import ( "crypto/x509/pkix" "fmt" + "time" "github.com/gibheer/pkiadm" ) type ( Subject struct { - ID string - Data pkix.Name + ID string + Data pkix.Name + Created time.Time } ) @@ -26,11 +28,14 @@ func NewSubject(id string, name pkix.Name) (*Subject, error) { // Return the unique ResourceName func (sub *Subject) Name() pkiadm.ResourceName { return pkiadm.ResourceName{sub.ID, pkiadm.RTSubject} } -// AddDependency registers a depending resource to be retuened by Dependencies() // Refresh must trigger a rebuild of the resource. // This is a NOOP as it does not have any dependencies. func (sub *Subject) Refresh(_ *Storage) error { return nil } +// RefreshInterval returns the dates and interval settings which are used to +// decide when to trigger a refresh for the resource. +func (sub *Subject) RefreshInterval() Interval { return Interval{Created: sub.Created} } + // Return the PEM output of the contained resource. func (sub *Subject) Pem() ([]byte, error) { return []byte{}, nil } func (sub *Subject) Checksum() []byte { return []byte{} } diff --git a/transport.go b/transport.go index 64abb25..f94f889 100644 --- a/transport.go +++ b/transport.go @@ -49,6 +49,21 @@ type ResourceType uint func (r ResourceName) String() string { return r.Type.String() + "/" + r.ID } +type ResourceNameList []ResourceName + +func (r ResourceNameList) Len() int { + return len(r) +} +func (r ResourceNameList) Less(i, j int) bool { + if r[i].Type != r[j].Type { + return r[i].Type < r[j].Type + } + return r[i].ID < r[j].ID +} +func (r ResourceNameList) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + type Filter struct{} type ResultResource struct { @@ -56,7 +71,7 @@ type ResultResource struct { Resources []ResourceName } -func (c *Client) List() ([]ResourceName, error) { +func (c *Client) List() (ResourceNameList, error) { result := ResultResource{} if err := c.query("List", Filter{}, &result); err != nil { return []ResourceName{}, err