2018-03-19 18:57:30 -04:00
|
|
|
package licensing
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/docker/licensing/model"
|
|
|
|
|
|
|
|
"github.com/docker/docker/api/types"
|
|
|
|
"github.com/docker/docker/api/types/filters"
|
|
|
|
"github.com/docker/docker/api/types/swarm"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
licenseNamePrefix = "com.docker.license"
|
|
|
|
licenseFilename = "docker.lic"
|
|
|
|
|
|
|
|
// ErrWorkerNode returned on a swarm worker node - lookup licenses on swarm managers
|
|
|
|
ErrWorkerNode = fmt.Errorf("this node is not a swarm manager - check license status on a manager node")
|
|
|
|
// ErrUnlicensed returned when no license found
|
|
|
|
ErrUnlicensed = fmt.Errorf("no license found")
|
|
|
|
)
|
|
|
|
|
|
|
|
// WrappedDockerClient provides methods useful for installing licenses to the wrapped docker engine or cluster
|
|
|
|
type WrappedDockerClient interface {
|
|
|
|
Info(ctx context.Context) (types.Info, error)
|
|
|
|
NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error)
|
|
|
|
ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
|
|
|
ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error)
|
|
|
|
ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// StoreLicense will store the license on the host filesystem and swarm (if swarm is active)
|
|
|
|
func StoreLicense(ctx context.Context, clnt WrappedDockerClient, license *model.IssuedLicense, rootDir string) error {
|
|
|
|
|
|
|
|
licenseData, err := json.Marshal(*license)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// First determine if we're in swarm-mode or a stand-alone engine
|
|
|
|
_, err = clnt.NodeList(ctx, types.NodeListOptions{})
|
|
|
|
if err != nil { // TODO - check for the specific error message
|
|
|
|
return writeLicenseToHost(ctx, clnt, licenseData, rootDir)
|
|
|
|
}
|
|
|
|
// Load this in the latest license index
|
|
|
|
latestVersion, err := getLatestNamedConfig(clnt, licenseNamePrefix)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to get latest license version: %s", err)
|
|
|
|
}
|
|
|
|
spec := swarm.ConfigSpec{
|
|
|
|
Annotations: swarm.Annotations{
|
|
|
|
Name: fmt.Sprintf("%s-%d", licenseNamePrefix, latestVersion+1),
|
|
|
|
Labels: map[string]string{
|
|
|
|
"com.docker.ucp.access.label": "/",
|
|
|
|
"com.docker.ucp.collection": "swarm",
|
|
|
|
"com.docker.ucp.collection.root": "true",
|
|
|
|
"com.docker.ucp.collection.swarm": "true",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Data: licenseData,
|
|
|
|
}
|
|
|
|
_, err = clnt.ConfigCreate(context.Background(), spec)
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return fmt.Errorf("Failed to create license: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) LoadLocalLicense(ctx context.Context, clnt WrappedDockerClient) (*model.Subscription, error) {
|
|
|
|
info, err := clnt.Info(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var licenseData []byte
|
|
|
|
if info.Swarm.LocalNodeState != "active" {
|
|
|
|
licenseData, err = readLicenseFromHost(ctx, info.DockerRootDir)
|
|
|
|
} else {
|
|
|
|
// Load the latest license index
|
2018-09-10 17:31:56 -04:00
|
|
|
var latestVersion int
|
|
|
|
latestVersion, err = getLatestNamedConfig(clnt, licenseNamePrefix)
|
2018-03-19 18:57:30 -04:00
|
|
|
if err != nil {
|
|
|
|
if strings.Contains(err.Error(), "not a swarm manager.") {
|
|
|
|
return nil, ErrWorkerNode
|
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("unable to get latest license version: %s", err)
|
|
|
|
}
|
2018-09-10 17:31:56 -04:00
|
|
|
if latestVersion >= 0 {
|
|
|
|
cfg, _, err := clnt.ConfigInspectWithRaw(ctx, fmt.Sprintf("%s-%d", licenseNamePrefix, latestVersion))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to load license from swarm config: %s", err)
|
|
|
|
}
|
|
|
|
licenseData = cfg.Spec.Data
|
|
|
|
} else {
|
|
|
|
licenseData, err = readLicenseFromHost(ctx, info.DockerRootDir)
|
2018-03-19 18:57:30 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil, ErrUnlicensed
|
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("Failed to create license: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
parsedLicense, err := c.ParseLicense(licenseData)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
checkResponse, err := c.VerifyLicense(ctx, *parsedLicense)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-09-10 17:31:56 -04:00
|
|
|
return checkResponseToSubscription(checkResponse, parsedLicense.KeyID), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkResponseToSubscription(checkResponse *model.CheckResponse, keyID string) *model.Subscription {
|
2018-03-19 18:57:30 -04:00
|
|
|
|
|
|
|
// TODO - this translation still needs some work
|
|
|
|
// Primary missing piece is how to distinguish from basic, vs std/advanced
|
|
|
|
var productID string
|
|
|
|
var ratePlan string
|
|
|
|
var state string
|
|
|
|
switch strings.ToLower(checkResponse.Tier) {
|
|
|
|
case "internal":
|
|
|
|
productID = "docker-ee-trial"
|
|
|
|
ratePlan = "free-trial"
|
|
|
|
case "production":
|
|
|
|
productID = "docker-ee"
|
|
|
|
if checkResponse.ScanningEnabled {
|
|
|
|
ratePlan = "nfr-advanced"
|
|
|
|
} else {
|
|
|
|
ratePlan = "nfr-standard"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine if the license has already expired
|
|
|
|
if checkResponse.Expiration.Before(time.Now()) {
|
|
|
|
state = "expired"
|
|
|
|
} else {
|
|
|
|
state = "active"
|
|
|
|
}
|
|
|
|
|
|
|
|
// Translate the legacy structure into the new Subscription fields
|
|
|
|
return &model.Subscription{
|
|
|
|
// Name
|
2018-09-10 17:31:56 -04:00
|
|
|
ID: keyID, // This is not actually the same, but is unique
|
2018-03-19 18:57:30 -04:00
|
|
|
// DockerID
|
|
|
|
ProductID: productID,
|
|
|
|
ProductRatePlan: ratePlan,
|
|
|
|
// ProductRatePlanID
|
|
|
|
// Start
|
|
|
|
Expires: &checkResponse.Expiration,
|
|
|
|
State: state,
|
|
|
|
// Eusa
|
|
|
|
PricingComponents: model.PricingComponents{
|
|
|
|
{
|
|
|
|
Name: "Nodes",
|
|
|
|
Value: checkResponse.MaxEngines,
|
|
|
|
},
|
|
|
|
},
|
2018-09-10 17:31:56 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) SummarizeLicense(checkResponse *model.CheckResponse, keyID string) *model.Subscription {
|
|
|
|
return checkResponseToSubscription(checkResponse, keyID)
|
2018-03-19 18:57:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// getLatestNamedConfig looks for versioned instances of configs with the
|
|
|
|
// given name prefix which have a `-NUM` integer version suffix. Returns the
|
|
|
|
// config with the higest version number found or nil if no such configs exist
|
|
|
|
// along with its version number.
|
|
|
|
func getLatestNamedConfig(dclient WrappedDockerClient, namePrefix string) (int, error) {
|
|
|
|
latestVersion := -1
|
|
|
|
// List any/all existing configs so that we create a newer version than
|
|
|
|
// any that already exist.
|
|
|
|
filter := filters.NewArgs()
|
|
|
|
filter.Add("name", namePrefix)
|
|
|
|
existingConfigs, err := dclient.ConfigList(context.Background(), types.ConfigListOptions{Filters: filter})
|
|
|
|
if err != nil {
|
|
|
|
return latestVersion, fmt.Errorf("unable to list existing configs: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, existingConfig := range existingConfigs {
|
|
|
|
existingConfigName := existingConfig.Spec.Name
|
|
|
|
nameSuffix := strings.TrimPrefix(existingConfigName, namePrefix)
|
|
|
|
if nameSuffix == "" || nameSuffix[0] != '-' {
|
|
|
|
continue // No version specifier?
|
|
|
|
}
|
|
|
|
|
|
|
|
versionSuffix := nameSuffix[1:] // Trim the version separator.
|
|
|
|
existingVersion, err := strconv.Atoi(versionSuffix)
|
|
|
|
if err != nil {
|
|
|
|
continue // Unable to parse version as integer.
|
|
|
|
}
|
|
|
|
if existingVersion > latestVersion {
|
|
|
|
latestVersion = existingVersion
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return latestVersion, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func writeLicenseToHost(ctx context.Context, dclient WrappedDockerClient, license []byte, rootDir string) error {
|
|
|
|
// TODO we should write the file out over the clnt instead of to the local filesystem
|
|
|
|
return ioutil.WriteFile(filepath.Join(rootDir, licenseFilename), license, 0644)
|
|
|
|
}
|
|
|
|
|
|
|
|
func readLicenseFromHost(ctx context.Context, rootDir string) ([]byte, error) {
|
|
|
|
// TODO we should read the file in over the clnt instead of to the local filesystem
|
|
|
|
return ioutil.ReadFile(filepath.Join(rootDir, licenseFilename))
|
|
|
|
}
|