DockerCLI/vendor/github.com/docker/licensing/storage.go

209 lines
6.5 KiB
Go
Raw Normal View History

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
latestVersion, err := getLatestNamedConfig(clnt, licenseNamePrefix)
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)
}
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
}
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
}
// 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
ID: parsedLicense.KeyID, // This is not actually the same, but is unique
// DockerID
ProductID: productID,
ProductRatePlan: ratePlan,
// ProductRatePlanID
// Start
Expires: &checkResponse.Expiration,
State: state,
// Eusa
PricingComponents: model.PricingComponents{
{
Name: "Nodes",
Value: checkResponse.MaxEngines,
},
},
}, nil
}
// 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))
}