From d64a65b407c27cf3e95fda76e31a1850a0d740d2 Mon Sep 17 00:00:00 2001 From: Zeke Gabrielse Date: Fri, 8 Sep 2023 12:21:01 -0500 Subject: [PATCH] add machine components --- README.md | 6 +-- client.go | 4 ++ component.go | 111 ++++++++++++++++++++++++++++++++++++++++++++++++ errors.go | 30 +++++++------ go.mod | 5 ++- go.sum | 10 +++-- keygen_test.go | 105 ++++++++++++++++++++++++++++++++++++++++++--- license.go | 31 +++++++++++--- machine.go | 37 ++++++++++++---- machine_file.go | 8 ++++ validate.go | 29 +++++++++---- 11 files changed, 326 insertions(+), 50 deletions(-) create mode 100644 component.go diff --git a/README.md b/README.md index d6c8509..c73adcf 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,9 @@ details. Then prompt the end-user for their license key or token and set `keygen or `keygen.Token`, respectively. The `Validate` method accepts zero or more fingerprints, which can be used to scope a license -validation to a particular fingerprint. It will return a `License` object as well as any -validation errors that occur. The `License` object can be used to perform additional actions, -such as `license.Activate(fingerprint)`. +validation to a particular device fingerprint and its hardware components. It will return a +`License` object as well as any validation errors that occur. The `License` object can be +used to perform additional actions, such as `license.Activate(fingerprint)`. ```go license, err := keygen.Validate(fingerprint) diff --git a/client.go b/client.go index 0544622..8fa50d0 100644 --- a/client.go +++ b/client.go @@ -387,6 +387,10 @@ func (c *Client) send(req *http.Request, model interface{}) (*Response, error) { return response, ErrMachineLimitExceeded case code == ErrorCodeProcessLimitExceeded: return response, ErrProcessLimitExceeded + case code == ErrorCodeComponentFingerprintConflict: + return response, ErrComponentConflict + case code == ErrorCodeComponentFingerprintTaken: + return response, ErrComponentAlreadyActivated case code == ErrorCodeTokenInvalid: return response, &LicenseTokenError{err} case code == ErrorCodeLicenseInvalid: diff --git a/component.go b/component.go new file mode 100644 index 0000000..aa0f5e0 --- /dev/null +++ b/component.go @@ -0,0 +1,111 @@ +package keygen + +import ( + "time" + + "github.com/keygen-sh/jsonapi-go" +) + +type component struct { + ID string `json:"-"` + Type string `json:"-"` + Fingerprint string `json:"fingerprint"` + Name string `json:"name"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + MachineID string `json:"-"` +} + +// GetID implements the jsonapi.MarshalResourceIdentifier interface. +func (c component) GetID() string { + return c.ID +} + +// GetType implements the jsonapi.MarshalResourceIdentifier interface. +func (c component) GetType() string { + return "components" +} + +// GetData implements the jsonapi.MarshalData interface. +func (c component) GetData() interface{} { + return c +} + +// GetRelationships implements jsonapi.MarshalRelationships interface. +func (c component) GetRelationships() map[string]interface{} { + relationships := make(map[string]interface{}) + + if c.MachineID != "" { + relationships["machine"] = jsonapi.ResourceObjectIdentifier{ + Type: "machines", + ID: c.MachineID, + } + } + + if len(relationships) == 0 { + return nil + } + + return relationships +} + +// Component represents a Keygen component object. +type Component struct { + ID string `json:"-"` + Type string `json:"-"` + Fingerprint string `json:"fingerprint"` + Name string `json:"name"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Metadata map[string]interface{} `json:"metadata"` + MachineID string `json:"-"` +} + +// GetID implements the jsonapi.MarshalResourceIdentifier interface. +func (c Component) GetID() string { + return c.ID +} + +// GetType implements the jsonapi.MarshalResourceIdentifier interface. +func (c Component) GetType() string { + return "components" +} + +// GetData implements the jsonapi.MarshalData interface. +func (c Component) GetData() interface{} { + // Transform public component to private component to only send a subset of attrs + return component{ + Fingerprint: c.Fingerprint, + Name: c.Name, + Metadata: c.Metadata, + MachineID: c.MachineID, + } +} + +func (c Component) UseExperimentalEmbeddedRelationshipData() bool { + return true +} + +// SetID implements the jsonapi.UnmarshalResourceIdentifier interface. +func (c *Component) SetID(id string) error { + c.ID = id + return nil +} + +// SetType implements the jsonapi.UnmarshalResourceIdentifier interface. +func (c *Component) SetType(t string) error { + c.Type = t + return nil +} + +// SetData implements the jsonapi.UnmarshalData interface. +func (c *Component) SetData(to func(target interface{}) error) error { + return to(c) +} + +// Components represents an array of component objects. +type Components []Component + +// SetData implements the jsonapi.UnmarshalData interface. +func (c *Components) SetData(to func(target interface{}) error) error { + return to(c) +} diff --git a/errors.go b/errors.go index 9f321e8..d72d23c 100644 --- a/errors.go +++ b/errors.go @@ -10,18 +10,20 @@ import ( type ErrorCode string const ( - ErrorCodeEnvironmentInvalid ErrorCode = "ENVIRONMENT_INVALID" - ErrorCodeEnvironmentNotSupported ErrorCode = "ENVIRONMENT_NOT_SUPPORTED" - ErrorCodeTokenInvalid ErrorCode = "TOKEN_INVALID" - ErrorCodeLicenseInvalid ErrorCode = "LICENSE_INVALID" - ErrorCodeLicenseExpired ErrorCode = "LICENSE_EXPIRED" - ErrorCodeLicenseSuspended ErrorCode = "LICENSE_SUSPENDED" - ErrorCodeFingerprintTaken ErrorCode = "FINGERPRINT_TAKEN" - ErrorCodeMachineLimitExceeded ErrorCode = "MACHINE_LIMIT_EXCEEDED" - ErrorCodeProcessLimitExceeded ErrorCode = "MACHINE_PROCESS_LIMIT_EXCEEDED" - ErrorCodeMachineHeartbeatDead ErrorCode = "MACHINE_HEARTBEAT_DEAD" - ErrorCodeProcessHeartbeatDead ErrorCode = "PROCESS_HEARTBEAT_DEAD" - ErrorCodeNotFound ErrorCode = "NOT_FOUND" + ErrorCodeEnvironmentInvalid ErrorCode = "ENVIRONMENT_INVALID" + ErrorCodeEnvironmentNotSupported ErrorCode = "ENVIRONMENT_NOT_SUPPORTED" + ErrorCodeTokenInvalid ErrorCode = "TOKEN_INVALID" + ErrorCodeLicenseInvalid ErrorCode = "LICENSE_INVALID" + ErrorCodeLicenseExpired ErrorCode = "LICENSE_EXPIRED" + ErrorCodeLicenseSuspended ErrorCode = "LICENSE_SUSPENDED" + ErrorCodeFingerprintTaken ErrorCode = "FINGERPRINT_TAKEN" + ErrorCodeMachineLimitExceeded ErrorCode = "MACHINE_LIMIT_EXCEEDED" + ErrorCodeProcessLimitExceeded ErrorCode = "MACHINE_PROCESS_LIMIT_EXCEEDED" + ErrorCodeComponentFingerprintConflict ErrorCode = "COMPONENTS_FINGERPRINT_CONFLICT" + ErrorCodeComponentFingerprintTaken ErrorCode = "COMPONENTS_FINGERPRINT_TAKEN" + ErrorCodeMachineHeartbeatDead ErrorCode = "MACHINE_HEARTBEAT_DEAD" + ErrorCodeProcessHeartbeatDead ErrorCode = "PROCESS_HEARTBEAT_DEAD" + ErrorCodeNotFound ErrorCode = "NOT_FOUND" ) // Error represents an API error response. @@ -116,6 +118,7 @@ var ( ErrPublicKeyMissing = errors.New("public key is missing") ErrPublicKeyInvalid = errors.New("public key is invalid") ErrValidationFingerprintMissing = errors.New("validation fingerprint scope is missing") + ErrValidationComponentsMissing = errors.New("validation components scope is missing") ErrValidationProductMissing = errors.New("validation product scope is missing") ErrHeartbeatPingFailed = errors.New("heartbeat ping failed") ErrHeartbeatRequired = errors.New("heartbeat is required") @@ -128,6 +131,9 @@ var ( ErrMachineFileNotEncrypted = errors.New("machine file is not encrypted") ErrMachineFileNotGenuine = errors.New("machine file is not genuine") ErrMachineFileExpired = errors.New("machine file is expired") + ErrComponentConflict = errors.New("component is duplicated") + ErrComponentAlreadyActivated = errors.New("component already exists") + ErrComponentNotActivated = errors.New("component is not activated") ErrProcessLimitExceeded = errors.New("process limit has been exceeded") ErrLicenseSchemeNotSupported = errors.New("license scheme is not supported") ErrLicenseSchemeMissing = errors.New("license scheme is missing") diff --git a/go.mod b/go.mod index 46605a2..3282450 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,13 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-retryablehttp v0.7.1 github.com/keygen-sh/go-update v1.0.0 - github.com/keygen-sh/jsonapi-go v1.2.0 + github.com/keygen-sh/jsonapi-go v1.2.1 github.com/oasisprotocol/curve25519-voi v0.0.0-20211102120939-d5a936accd94 ) require ( golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e // indirect golang.org/x/net v0.0.0-20210521195947-fe42d452be8f // indirect - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect + golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index ba84e98..35a88f5 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/keygen-sh/go-update v1.0.0 h1:M65sTVUHUO07tEK4l1Hq7u5D4kdEqkcgfdzUt3q3S08= github.com/keygen-sh/go-update v1.0.0/go.mod h1:wn0UWRHLnBP5hwXtj1IdHZqWlHvIadh2Nn+becFf8Ro= -github.com/keygen-sh/jsonapi-go v1.2.0 h1:BXVX9YAZt4itgTs9sV0ame0/+6WQMZHhbF8f62paNwo= -github.com/keygen-sh/jsonapi-go v1.2.0/go.mod h1:8j9vsLiKyJyDqmt8r3tYaYNmXszq2+cFhoO6QdMdAes= +github.com/keygen-sh/jsonapi-go v1.2.1 h1:NTSIAxl2+7S5fPnKgrYwNjQSWbdKRtrFq26SD8AOkiU= +github.com/keygen-sh/jsonapi-go v1.2.1/go.mod h1:8j9vsLiKyJyDqmt8r3tYaYNmXszq2+cFhoO6QdMdAes= github.com/oasisprotocol/curve25519-voi v0.0.0-20211102120939-d5a936accd94 h1:YXfl+eCNmAQhVbSNQ85bSi1n4qhUBPW8Qq9Rac4pt/s= github.com/oasisprotocol/curve25519-voi v0.0.0-20211102120939-d5a936accd94/go.mod h1:WUcXjUd98qaCVFb6j8Xc87MsKeMCXDu9Nk8JRJ9SeC8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -46,8 +46,9 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -64,5 +65,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/keygen_test.go b/keygen_test.go index 6fd8398..975a141 100644 --- a/keygen_test.go +++ b/keygen_test.go @@ -36,6 +36,10 @@ func TestValidate(t *testing.T) { t.Fatalf("Should fingerprint the current machine: err=%v", err) } + if _, err := Validate(); err != ErrValidationFingerprintMissing { + t.Fatalf("Should have a required scope: err=%v", err) + } + license, err := Validate(fingerprint) if err == nil { t.Fatalf("Should not be activated: err=%v", err) @@ -107,7 +111,7 @@ func TestValidate(t *testing.T) { case dataset.License.ID != license.ID: t.Fatalf("Should have the correct license ID: actual=%s expected=%s", dataset.License.ID, license.ID) case len(dataset.Entitlements) == 0: - t.Fatalf("Should have at least 1 entitlement: entitlements=%s", dataset.Entitlements) + t.Fatalf("Should have at least 1 entitlement: entitlements=%v", dataset.Entitlements) case dataset.Issued.IsZero(): t.Fatalf("Should have an issued timestamp: ts=%v", dataset.Issued) case dataset.Expiry.IsZero(): @@ -146,7 +150,7 @@ func TestValidate(t *testing.T) { case dataset.License.ID != license.ID: t.Fatalf("Should have the correct license ID: actual=%s expected=%s", dataset.License.ID, license.ID) case len(dataset.Entitlements) != 0: - t.Fatalf("Should have no entitlements: entitlements=%s", dataset.Entitlements) + t.Fatalf("Should have no entitlements: entitlements=%v", dataset.Entitlements) case dataset.Issued.IsZero(): t.Fatalf("Should have an issued timestamp: ts=%v", dataset.Issued) case time.Until(dataset.Expiry) > 24*time.Hour+30*time.Second: // 30s for network lag @@ -189,7 +193,7 @@ func TestValidate(t *testing.T) { case dataset.License.ID != license.ID: t.Fatalf("Should have the correct license ID: actual=%s expected=%s", dataset.License.ID, license.ID) case len(dataset.Entitlements) == 0: - t.Fatalf("Should have at least 1 entitlement: entitlements=%s", dataset.Entitlements) + t.Fatalf("Should have at least 1 entitlement: entitlements=%v", dataset.Entitlements) case dataset.Issued.IsZero(): t.Fatalf("Should have an issued timestamp: ts=%v", dataset.Issued) case dataset.Expiry.IsZero(): @@ -204,7 +208,7 @@ func TestValidate(t *testing.T) { // options { mic, err := machine.Checkout( - CheckoutInclude("license"), + CheckoutInclude("license", "components"), CheckoutTTL(24*time.Hour*365), ) if err != nil { @@ -230,7 +234,9 @@ func TestValidate(t *testing.T) { case dataset.License.ID != license.ID: t.Fatalf("Should have the correct license ID: actual=%s expected=%s", dataset.License.ID, license.ID) case len(dataset.Entitlements) != 0: - t.Fatalf("Should have no entitlements: entitlements=%s", dataset.Entitlements) + t.Fatalf("Should have no entitlements: entitlements=%v", dataset.Entitlements) + case len(dataset.Components) != 0: + t.Fatalf("Should have no components: components=%v", dataset.Components) case dataset.Issued.IsZero(): t.Fatalf("Should have an issued timestamp: ts=%v", dataset.Issued) case time.Until(dataset.Expiry) < 24*time.Hour*365: @@ -353,6 +359,95 @@ func TestValidate(t *testing.T) { t.Fatalf("Should not fail to list entitlements: err=%v", err) } + if len(entitlements) == 0 { + t.Fatalf("Should have entitlements: entitlements=%v", entitlements) + } + + // Components + { + board := uuid.NewString() + disk := uuid.NewString() + cpu := uuid.NewString() + gpu := uuid.NewString() + + machine, err := license.Activate(fingerprint, + Component{Name: "Board", Fingerprint: board}, + Component{Name: "Drive", Fingerprint: disk}, + Component{Name: "CPU", Fingerprint: cpu}, + Component{Name: "GPU", Fingerprint: gpu}, + ) + if err != nil { + t.Fatalf("Should not fail reactivation: err=%v", err) + } + + components, err := machine.Components() + if err != nil { + t.Fatalf("Should not fail to list components: err=%v", err) + } + + if len(components) == 0 { + t.Fatalf("Should have components: components=%v", components) + } + + license, err = Validate(fingerprint, board, disk, gpu, cpu) + if err != nil { + t.Fatalf("Should be valid: err=%v", err) + } + + switch { + case license.LastValidation.Scope.Fingerprint != fingerprint: + t.Fatalf("Should be scoped to fingerprint: scope=%v", license.LastValidation) + case len(license.LastValidation.Scope.Components) != 4: + t.Fatalf("Should be scoped to components: scope=%v", license.LastValidation) + } + + if _, err = Validate(fingerprint, uuid.NewString()); err != ErrComponentNotActivated { + t.Fatalf("Should be invalid: err=%v", err) + } + + mic, err := machine.Checkout( + CheckoutInclude("components", "license", "license.entitlements"), + ) + if err != nil { + t.Fatalf("Should not fail checkout: err=%v", err) + } + + err = mic.Verify() + switch { + case err == ErrLicenseFileNotGenuine: + t.Fatalf("Should be a genuine machine file: err=%v", err) + case err != nil: + t.Fatalf("Should not fail genuine check: err=%v", err) + } + + dataset, err := mic.Decrypt(license.Key + machine.Fingerprint) + if err != nil { + t.Fatalf("Should not fail decrypt: err=%v", err) + } + + switch { + case dataset.Machine.ID != machine.ID: + t.Fatalf("Should have the correct machine ID: actual=%s expected=%s", dataset.Machine.ID, machine.ID) + case dataset.License.ID != license.ID: + t.Fatalf("Should have the correct license ID: actual=%s expected=%s", dataset.License.ID, license.ID) + case len(dataset.Entitlements) == 0: + t.Fatalf("Should have entitlements: entitlements=%v", dataset.Entitlements) + case len(dataset.Components) != 4: + t.Fatalf("Should have components: components=%v", dataset.Components) + case dataset.Issued.IsZero(): + t.Fatalf("Should have an issued timestamp: ts=%v", dataset.Issued) + case dataset.Expiry.IsZero(): + t.Fatalf("Should have an expiry timestamp: ts=%v", dataset.Expiry) + case dataset.TTL == 0: + t.Fatalf("Should have a TTL: ttl=%d", dataset.TTL) + } + + err = machine.Deactivate() + if err != nil { + t.Fatalf("Should not fail deactivation: err=%v", err) + } + } + t.Logf( "license=%+v machines=%+v entitlements=%+v", license, diff --git a/license.go b/license.go index abab641..3844426 100644 --- a/license.go +++ b/license.go @@ -57,14 +57,26 @@ func (l *License) SetRelationships(relationships map[string]interface{}) error { return nil } -// Validate performs a license validation, scoped to any provided fingerprints. It -// returns an error if the license is invalid, e.g. ErrLicenseNotActivated, -// ErrLicenseExpired or ErrLicenseTooManyMachines. +// Validate performs a license validation, scoped to an optional device fingerprint +// and an optional array of hardware component fingerprints. It returns an error +// if the license is invalid, e.g. ErrLicenseNotActivated, ErrLicenseExpired or +// ErrLicenseTooManyMachines. func (l *License) Validate(fingerprints ...string) error { client := NewClient() - params := &validate{fingerprints} validation := &validation{} + // split up fingerprints (first is machine, rest are components) + var params validate + if n := len(fingerprints); n > 0 { + if n > 1 { + params = validate{fingerprint: fingerprints[0], components: fingerprints[1:]} + } else { + params = validate{fingerprint: fingerprints[0]} + } + } else { + params = validate{} + } + if _, err := client.Post("licenses/"+l.ID+"/actions/validate", params, validation); err != nil { if _, ok := err.(*NotFoundError); ok { return ErrLicenseInvalid @@ -100,6 +112,11 @@ func (l *License) Validate(fingerprints ...string) error { case validation.Result.Code == ValidationCodeFingerprintScopeRequired || validation.Result.Code == ValidationCodeFingerprintScopeEmpty: return ErrValidationFingerprintMissing + case validation.Result.Code == ValidationCodeComponentsScopeRequired || + validation.Result.Code == ValidationCodeComponentsScopeEmpty: + return ErrValidationComponentsMissing + case validation.Result.Code == ValidationCodeComponentsScopeMismatch: + return ErrComponentNotActivated case validation.Result.Code == ValidationCodeHeartbeatNotStarted: return ErrHeartbeatRequired case validation.Result.Code == ValidationCodeHeartbeatDead: @@ -130,7 +147,7 @@ func (l *License) Verify() ([]byte, error) { // fingerprint. If the activation is successful, the new machine will be returned. An // error will be returned if the activation fails, e.g. ErrMachineLimitExceeded // or ErrMachineAlreadyActivated. -func (l *License) Activate(fingerprint string) (*Machine, error) { +func (l *License) Activate(fingerprint string, components ...Component) (*Machine, error) { client := NewClient() hostname, _ := os.Hostname() params := &Machine{ @@ -139,6 +156,7 @@ func (l *License) Activate(fingerprint string) (*Machine, error) { Platform: runtime.GOOS + "/" + runtime.GOARCH, Cores: runtime.NumCPU(), LicenseID: l.ID, + components: components, } machine := &Machine{} @@ -207,8 +225,7 @@ func (l *License) Checkout(options ...CheckoutOption) (*LicenseFile, error) { opts := CheckoutOptions{Encrypt: true, Include: "entitlements"} for _, opt := range options { - err := opt(&opts) - if err != nil { + if err := opt(&opts); err != nil { return nil, err } } diff --git a/machine.go b/machine.go index c2529bf..7cf05e9 100644 --- a/machine.go +++ b/machine.go @@ -16,13 +16,14 @@ const ( ) type machine struct { - ID string `json:"-"` - Type string `json:"-"` - Fingerprint string `json:"fingerprint"` - Hostname string `json:"hostname"` - Platform string `json:"platform"` - Cores int `json:"cores"` - LicenseID string `json:"-"` + ID string `json:"-"` + Type string `json:"-"` + Fingerprint string `json:"fingerprint"` + Hostname string `json:"hostname"` + Platform string `json:"platform"` + Cores int `json:"cores"` + LicenseID string `json:"-"` + Components Components `json:"-"` } // GetID implements the jsonapi.MarshalResourceIdentifier interface. @@ -44,6 +45,10 @@ func (m machine) GetData() interface{} { func (m machine) GetRelationships() map[string]interface{} { relationships := make(map[string]interface{}) + if len(m.Components) > 0 { + relationships["components"] = m.Components + } + relationships["license"] = jsonapi.ResourceObjectIdentifier{ Type: "licenses", ID: m.LicenseID, @@ -68,6 +73,8 @@ type Machine struct { Updated time.Time `json:"updated"` Metadata map[string]interface{} `json:"metadata"` LicenseID string `json:"-"` + + components []Component `json:"-"` } // GetID implements the jsonapi.MarshalResourceIdentifier interface. @@ -89,6 +96,7 @@ func (m Machine) GetData() interface{} { Platform: m.Platform, Cores: m.Cores, LicenseID: m.LicenseID, + Components: m.components, } } @@ -159,8 +167,7 @@ func (m *Machine) Checkout(options ...CheckoutOption) (*MachineFile, error) { opts := CheckoutOptions{Encrypt: true, Include: "license,license.entitlements"} for _, opt := range options { - err := opt(&opts) - if err != nil { + if err := opt(&opts); err != nil { return nil, err } } @@ -176,6 +183,18 @@ func (m *Machine) Checkout(options ...CheckoutOption) (*MachineFile, error) { return lic, nil } +// Components lists up to 100 components for the machine. +func (m *Machine) Components() (Components, error) { + client := NewClient() + components := Components{} + + if _, err := client.Get("machines/"+m.ID+"/components", querystring{Limit: 100}, &components); err != nil { + return nil, err + } + + return components, nil +} + // Spawn creates a new process for a machine, identified by the provided pid. If // successful, the new Process will be returned. When unsuccessful, as error // will be returned, e.g. ErrProcessLimitExceeded. Automatically starts a loop diff --git a/machine_file.go b/machine_file.go index c92c479..30d6ecf 100644 --- a/machine_file.go +++ b/machine_file.go @@ -130,6 +130,7 @@ type MachineFileDataset struct { Machine Machine `json:"-"` License License `json:"-"` Entitlements Entitlements `json:"-"` + Components Components `json:"-"` Issued time.Time `json:"issued"` Expiry time.Time `json:"expiry"` TTL int `json:"ttl"` @@ -149,6 +150,13 @@ func (lic *MachineFileDataset) SetMeta(to func(target interface{}) error) error func (lic *MachineFileDataset) SetIncluded(relationships []*jsonapi.ResourceObject, unmarshal func(res *jsonapi.ResourceObject, target interface{}) error) error { for _, relationship := range relationships { switch relationship.Type { + case "components": + component := &Component{} + if err := unmarshal(relationship, component); err != nil { + return err + } + + lic.Components = append(lic.Components, *component) case "entitlements": entitlement := &Entitlement{} if err := unmarshal(relationship, entitlement); err != nil { diff --git a/validate.go b/validate.go index 749bc5e..e5ae778 100644 --- a/validate.go +++ b/validate.go @@ -16,6 +16,9 @@ const ( ValidationCodeFingerprintScopeRequired ValidationCode = "FINGERPRINT_SCOPE_REQUIRED" ValidationCodeFingerprintScopeMismatch ValidationCode = "FINGERPRINT_SCOPE_MISMATCH" ValidationCodeFingerprintScopeEmpty ValidationCode = "FINGERPRINT_SCOPE_EMPTY" + ValidationCodeComponentsScopeRequired ValidationCode = "COMPONENTS_SCOPE_REQUIRED" + ValidationCodeComponentsScopeMismatch ValidationCode = "COMPONENTS_SCOPE_MISMATCH" + ValidationCodeComponentsScopeEmpty ValidationCode = "COMPONENTS_SCOPE_EMPTY" ValidationCodeHeartbeatNotStarted ValidationCode = "HEARTBEAT_NOT_STARTED" ValidationCodeHeartbeatDead ValidationCode = "HEARTBEAT_DEAD" ValidationCodeProductScopeRequired ValidationCode = "PRODUCT_SCOPE_REQUIRED" @@ -29,7 +32,8 @@ const ( ) type validate struct { - fingerprints []string + fingerprint string + components []string } type meta struct { @@ -37,18 +41,19 @@ type meta struct { } type scope struct { - Fingerprints []string `json:"fingerprints"` - Product string `json:"product"` - Environment *string `json:"environment,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Components []string `json:"components,omitempty"` + Product string `json:"product"` + Environment *string `json:"environment,omitempty"` } // GetMeta implements jsonapi.MarshalMeta interface. func (v validate) GetMeta() interface{} { if Environment != "" { - return meta{Scope: scope{Fingerprints: v.fingerprints, Product: Product, Environment: &Environment}} + return meta{Scope: scope{Fingerprint: v.fingerprint, Components: v.components, Product: Product, Environment: &Environment}} } - return meta{Scope: scope{Fingerprints: v.fingerprints, Environment: nil, Product: Product}} + return meta{Scope: scope{Fingerprint: v.fingerprint, Components: v.components, Environment: nil, Product: Product}} } type validation struct { @@ -66,9 +71,17 @@ func (v *validation) SetMeta(to func(target interface{}) error) error { return to(&v.Result) } +// ValidationResult contains the scopes for a validation. +type ValidationScope struct { + scope +} + +// ValidationResult is the result of the validation. type ValidationResult struct { - Code ValidationCode `json:"code"` - Valid bool `json:"valid"` + Detail string `json:"detail"` + Valid bool `json:"valid"` + Code ValidationCode `json:"code"` + Scope *ValidationScope `json:"scope,omitempty"` } // Validate performs a license validation using the current Token, scoped to any