package ca

import (
	"context"
	cryptorand "crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"math/big"
	"math/rand"
	"path/filepath"
	"strings"
	"sync"
	"time"

	cfconfig "github.com/cloudflare/cfssl/config"
	events "github.com/docker/go-events"
	"github.com/opencontainers/go-digest"
	"github.com/pkg/errors"
	"google.golang.org/grpc/credentials"

	"github.com/moby/swarmkit/v2/api"
	"github.com/moby/swarmkit/v2/connectionbroker"
	"github.com/moby/swarmkit/v2/identity"
	"github.com/moby/swarmkit/v2/log"
	"github.com/moby/swarmkit/v2/watch"
)

const (
	rootCACertFilename  = "swarm-root-ca.crt"
	rootCAKeyFilename   = "swarm-root-ca.key"
	nodeTLSCertFilename = "swarm-node.crt"
	nodeTLSKeyFilename  = "swarm-node.key"

	// DefaultRootCN represents the root CN that we should create roots CAs with by default
	DefaultRootCN = "swarm-ca"
	// ManagerRole represents the Manager node type, and is used for authorization to endpoints
	ManagerRole = "swarm-manager"
	// WorkerRole represents the Worker node type, and is used for authorization to endpoints
	WorkerRole = "swarm-worker"
	// CARole represents the CA node type, and is used for clients attempting to get new certificates issued
	CARole = "swarm-ca"

	generatedSecretEntropyBytes = 16
	joinTokenBase               = 36
	// ceil(log(2^128-1, 36))
	maxGeneratedSecretLength = 25
	// ceil(log(2^256-1, 36))
	base36DigestLen = 50
)

var (
	// GetCertRetryInterval is how long to wait before retrying a node
	// certificate or root certificate request.
	GetCertRetryInterval = 2 * time.Second

	// errInvalidJoinToken is returned when attempting to parse an invalid join
	// token (e.g. when attempting to get the version, fipsness, or the root ca
	// digest)
	errInvalidJoinToken = errors.New("invalid join token")
)

// SecurityConfig is used to represent a node's security configuration. It includes information about
// the RootCA and ServerTLSCreds/ClientTLSCreds transport authenticators to be used for MTLS
type SecurityConfig struct {
	// mu protects against concurrent access to fields inside the structure.
	mu sync.Mutex

	// renewalMu makes sure only one certificate renewal attempt happens at
	// a time. It should never be locked after mu is already locked.
	renewalMu sync.Mutex

	rootCA        *RootCA
	keyReadWriter *KeyReadWriter

	certificate *tls.Certificate
	issuerInfo  *IssuerInfo

	ServerTLSCreds *MutableTLSCreds
	ClientTLSCreds *MutableTLSCreds

	// An optional queue for anyone interested in subscribing to SecurityConfig updates
	queue *watch.Queue
}

// CertificateUpdate represents a change in the underlying TLS configuration being returned by
// a certificate renewal event.
type CertificateUpdate struct {
	Role string
	Err  error
}

// ParsedJoinToken is the data from a join token, once parsed
type ParsedJoinToken struct {
	// Version is the version of the join token that is being parsed
	Version int

	// RootDigest is the digest of the root CA certificate of the cluster, which
	// is always part of the join token so that the root CA can be verified
	// upon initial node join
	RootDigest digest.Digest

	// Secret is the randomly-generated secret part of the join token - when
	// rotating a join token, this is the value that is changed unless some other
	// property of the cluster (like the root CA) is changed.
	Secret string

	// FIPS indicates whether the join token specifies that the cluster mandates
	// that all nodes must have FIPS mode enabled.
	FIPS bool
}

// ParseJoinToken parses a join token.  Current format is v2, but this is currently used only if the cluster requires
// mandatory FIPS, in order to facilitate mixed version clusters.
// v1: SWMTKN-1-<SHA256 digest of root CA cert in base 36, 0-left-padded to 50 chars>-<16-byte secret in base 36 0-left-padded to 25 chars>
// v2: SWMTKN-2-<0/1 whether its FIPS or not>-<same rest of data as v1>
func ParseJoinToken(token string) (*ParsedJoinToken, error) {
	split := strings.Split(token, "-")
	numParts := len(split)

	// v1 has 4, v2 has 5
	if numParts < 4 || split[0] != "SWMTKN" {
		return nil, errInvalidJoinToken
	}

	var (
		version int
		fips    bool
	)

	switch split[1] {
	case "1":
		if numParts != 4 {
			return nil, errInvalidJoinToken
		}
		version = 1
	case "2":
		if numParts != 5 || (split[2] != "0" && split[2] != "1") {
			return nil, errInvalidJoinToken
		}
		version = 2
		fips = split[2] == "1"
	default:
		return nil, errInvalidJoinToken
	}

	secret := split[numParts-1]
	rootDigest := split[numParts-2]
	if len(rootDigest) != base36DigestLen || len(secret) != maxGeneratedSecretLength {
		return nil, errInvalidJoinToken
	}

	var digestInt big.Int
	digestInt.SetString(rootDigest, joinTokenBase)

	d, err := digest.Parse(fmt.Sprintf("sha256:%0[1]*s", 64, digestInt.Text(16)))
	if err != nil {
		return nil, err
	}
	return &ParsedJoinToken{
		Version:    version,
		RootDigest: d,
		Secret:     secret,
		FIPS:       fips,
	}, nil
}

func validateRootCAAndTLSCert(rootCA *RootCA, tlsKeyPair *tls.Certificate) error {
	var (
		leafCert         *x509.Certificate
		intermediatePool *x509.CertPool
	)
	for i, derBytes := range tlsKeyPair.Certificate {
		parsed, err := x509.ParseCertificate(derBytes)
		if err != nil {
			return errors.Wrap(err, "could not validate new root certificates due to parse error")
		}
		if i == 0 {
			leafCert = parsed
		} else {
			if intermediatePool == nil {
				intermediatePool = x509.NewCertPool()
			}
			intermediatePool.AddCert(parsed)
		}
	}
	opts := x509.VerifyOptions{
		Roots:         rootCA.Pool,
		Intermediates: intermediatePool,
	}
	if _, err := leafCert.Verify(opts); err != nil {
		return errors.Wrap(err, "new root CA does not match existing TLS credentials")
	}
	return nil
}

// NewSecurityConfig initializes and returns a new SecurityConfig.
func NewSecurityConfig(rootCA *RootCA, krw *KeyReadWriter, tlsKeyPair *tls.Certificate, issuerInfo *IssuerInfo) (*SecurityConfig, func() error, error) {
	// Create the Server TLS Credentials for this node. These will not be used by workers.
	serverTLSCreds, err := rootCA.NewServerTLSCredentials(tlsKeyPair)
	if err != nil {
		return nil, nil, err
	}

	// Create a TLSConfig to be used when this node connects as a client to another remote node.
	// We're using ManagerRole as remote serverName for TLS host verification because both workers
	// and managers always connect to remote managers.
	clientTLSCreds, err := rootCA.NewClientTLSCredentials(tlsKeyPair, ManagerRole)
	if err != nil {
		return nil, nil, err
	}

	q := watch.NewQueue()
	return &SecurityConfig{
		rootCA:        rootCA,
		keyReadWriter: krw,

		certificate: tlsKeyPair,
		issuerInfo:  issuerInfo,
		queue:       q,

		ClientTLSCreds: clientTLSCreds,
		ServerTLSCreds: serverTLSCreds,
	}, q.Close, nil
}

// RootCA returns the root CA.
func (s *SecurityConfig) RootCA() *RootCA {
	s.mu.Lock()
	defer s.mu.Unlock()

	return s.rootCA
}

// KeyWriter returns the object that can write keys to disk
func (s *SecurityConfig) KeyWriter() KeyWriter {
	return s.keyReadWriter
}

// KeyReader returns the object that can read keys from disk
func (s *SecurityConfig) KeyReader() KeyReader {
	return s.keyReadWriter
}

// UpdateRootCA replaces the root CA with a new root CA
func (s *SecurityConfig) UpdateRootCA(rootCA *RootCA) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	// refuse to update the root CA if the current TLS credentials do not validate against it
	if err := validateRootCAAndTLSCert(rootCA, s.certificate); err != nil {
		return err
	}

	s.rootCA = rootCA
	return s.updateTLSCredentials(s.certificate, s.issuerInfo)
}

// Watch allows you to set a watch on the security config, in order to be notified of any changes
func (s *SecurityConfig) Watch() (chan events.Event, func()) {
	return s.queue.Watch()
}

// IssuerInfo returns the issuer subject and issuer public key
func (s *SecurityConfig) IssuerInfo() *IssuerInfo {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.issuerInfo
}

// This function expects something else to have taken out a lock on the SecurityConfig.
func (s *SecurityConfig) updateTLSCredentials(certificate *tls.Certificate, issuerInfo *IssuerInfo) error {
	certs := []tls.Certificate{*certificate}
	clientConfig, err := NewClientTLSConfig(certs, s.rootCA.Pool, ManagerRole)
	if err != nil {
		return errors.Wrap(err, "failed to create a new client config using the new root CA")
	}

	serverConfig, err := NewServerTLSConfig(certs, s.rootCA.Pool)
	if err != nil {
		return errors.Wrap(err, "failed to create a new server config using the new root CA")
	}

	if err := s.ClientTLSCreds.loadNewTLSConfig(clientConfig); err != nil {
		return errors.Wrap(err, "failed to update the client credentials")
	}

	if err := s.ServerTLSCreds.loadNewTLSConfig(serverConfig); err != nil {
		return errors.Wrap(err, "failed to update the server TLS credentials")
	}

	s.certificate = certificate
	s.issuerInfo = issuerInfo
	if s.queue != nil {
		s.queue.Publish(&api.NodeTLSInfo{
			TrustRoot:           s.rootCA.Certs,
			CertIssuerPublicKey: s.issuerInfo.PublicKey,
			CertIssuerSubject:   s.issuerInfo.Subject,
		})
	}
	return nil
}

// UpdateTLSCredentials updates the security config with an updated TLS certificate and issuer info
func (s *SecurityConfig) UpdateTLSCredentials(certificate *tls.Certificate, issuerInfo *IssuerInfo) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.updateTLSCredentials(certificate, issuerInfo)
}

// SigningPolicy creates a policy used by the signer to ensure that the only fields
// from the remote CSRs we trust are: PublicKey, PublicKeyAlgorithm and SignatureAlgorithm.
// It receives the duration a certificate will be valid for
func SigningPolicy(certExpiry time.Duration) *cfconfig.Signing {
	// Force the minimum Certificate expiration to be fifteen minutes
	if certExpiry < MinNodeCertExpiration {
		certExpiry = DefaultNodeCertExpiration
	}

	// Add the backdate
	certExpiry = certExpiry + CertBackdate

	return &cfconfig.Signing{
		Default: &cfconfig.SigningProfile{
			Usage:    []string{"signing", "key encipherment", "server auth", "client auth"},
			Expiry:   certExpiry,
			Backdate: CertBackdate,
			// Only trust the key components from the CSR. Everything else should
			// come directly from API call params.
			CSRWhitelist: &cfconfig.CSRWhitelist{
				PublicKey:          true,
				PublicKeyAlgorithm: true,
				SignatureAlgorithm: true,
			},
		},
	}
}

// SecurityConfigPaths is used as a helper to hold all the paths of security relevant files
type SecurityConfigPaths struct {
	Node, RootCA CertPaths
}

// NewConfigPaths returns the absolute paths to all of the different types of files
func NewConfigPaths(baseCertDir string) *SecurityConfigPaths {
	return &SecurityConfigPaths{
		Node: CertPaths{
			Cert: filepath.Join(baseCertDir, nodeTLSCertFilename),
			Key:  filepath.Join(baseCertDir, nodeTLSKeyFilename)},
		RootCA: CertPaths{
			Cert: filepath.Join(baseCertDir, rootCACertFilename),
			Key:  filepath.Join(baseCertDir, rootCAKeyFilename)},
	}
}

// GenerateJoinToken creates a new join token. Current format is v2, but this is
// currently used only if the cluster requires mandatory FIPS, in order to
// facilitate mixed version clusters (the `fips` parameter is set to true).
// Otherwise, v1 is used so as to maintain compatibility in mixed version
// non-FIPS clusters.
// v1: SWMTKN-1-<SHA256 digest of root CA cert in base 36, 0-left-padded to 50 chars>-<16-byte secret in base 36 0-left-padded to 25 chars>
// v2: SWMTKN-2-<0/1 whether its FIPS or not>-<same rest of data as v1>
func GenerateJoinToken(rootCA *RootCA, fips bool) string {
	var secretBytes [generatedSecretEntropyBytes]byte

	if _, err := cryptorand.Read(secretBytes[:]); err != nil {
		panic(fmt.Errorf("failed to read random bytes: %v", err))
	}

	var nn, dgst big.Int
	nn.SetBytes(secretBytes[:])
	dgst.SetString(rootCA.Digest.Encoded(), 16)

	fmtString := "SWMTKN-1-%0[1]*s-%0[3]*s"
	if fips {
		fmtString = "SWMTKN-2-1-%0[1]*s-%0[3]*s"
	}
	return fmt.Sprintf(fmtString, base36DigestLen,
		dgst.Text(joinTokenBase), maxGeneratedSecretLength, nn.Text(joinTokenBase))
}

// DownloadRootCA tries to retrieve a remote root CA and matches the digest against the provided token.
func DownloadRootCA(ctx context.Context, paths CertPaths, token string, connBroker *connectionbroker.Broker) (RootCA, error) {
	var rootCA RootCA
	// Get a digest for the optional CA hash string that we've been provided
	// If we were provided a non-empty string, and it is an invalid hash, return
	// otherwise, allow the invalid digest through.
	var (
		d   digest.Digest
		err error
	)
	if token != "" {
		parsed, err := ParseJoinToken(token)
		if err != nil {
			return RootCA{}, err
		}
		d = parsed.RootDigest
	}
	// Get the remote CA certificate, verify integrity with the
	// hash provided. Retry up to 5 times, in case the manager we
	// first try to contact is not responding properly (it may have
	// just been demoted, for example).

	for i := 0; i != 5; i++ {
		rootCA, err = GetRemoteCA(ctx, d, connBroker)
		if err == nil {
			break
		}
		log.G(ctx).WithError(err).Errorf("failed to retrieve remote root CA certificate")

		select {
		case <-time.After(GetCertRetryInterval):
		case <-ctx.Done():
			return RootCA{}, ctx.Err()
		}
	}
	if err != nil {
		return RootCA{}, err
	}

	// Save root CA certificate to disk
	if err = SaveRootCA(rootCA, paths); err != nil {
		return RootCA{}, err
	}

	log.G(ctx).Debugf("retrieved remote CA certificate: %s", paths.Cert)
	return rootCA, nil
}

// LoadSecurityConfig loads TLS credentials from disk, or returns an error if
// these credentials do not exist or are unusable.
func LoadSecurityConfig(ctx context.Context, rootCA RootCA, krw *KeyReadWriter, allowExpired bool) (*SecurityConfig, func() error, error) {
	ctx = log.WithModule(ctx, "tls")

	// At this point we've successfully loaded the CA details from disk, or
	// successfully downloaded them remotely. The next step is to try to
	// load our certificates.

	// Read both the Cert and Key from disk
	cert, key, err := krw.Read()
	if err != nil {
		return nil, nil, err
	}

	// Check to see if this certificate was signed by our CA, and isn't expired
	_, chains, err := ValidateCertChain(rootCA.Pool, cert, allowExpired)
	if err != nil {
		return nil, nil, err
	}
	// ValidateChain, if successful, will always return at least 1 chain containing
	// at least 2 certificates:  the leaf and the root.
	issuer := chains[0][1]

	// Now that we know this certificate is valid, create a TLS Certificate for our
	// credentials
	keyPair, err := tls.X509KeyPair(cert, key)
	if err != nil {
		return nil, nil, err
	}

	secConfig, cleanup, err := NewSecurityConfig(&rootCA, krw, &keyPair, &IssuerInfo{
		Subject:   issuer.RawSubject,
		PublicKey: issuer.RawSubjectPublicKeyInfo,
	})
	if err == nil {
		log.G(ctx).WithFields(log.Fields{
			"node.id":   secConfig.ClientTLSCreds.NodeID(),
			"node.role": secConfig.ClientTLSCreds.Role(),
		}).Debug("loaded node credentials")
	}
	return secConfig, cleanup, err
}

// CertificateRequestConfig contains the information needed to request a
// certificate from a remote CA.
type CertificateRequestConfig struct {
	// Token is the join token that authenticates us with the CA.
	Token string
	// Availability allows a user to control the current scheduling status of a node
	Availability api.NodeSpec_Availability
	// ConnBroker provides connections to CAs.
	ConnBroker *connectionbroker.Broker
	// Credentials provides transport credentials for communicating with the
	// remote server.
	Credentials credentials.TransportCredentials
	// ForceRemote specifies that only a remote (TCP) connection should
	// be used to request the certificate. This may be necessary in cases
	// where the local node is running a manager, but is in the process of
	// being demoted.
	ForceRemote bool
	// NodeCertificateStatusRequestTimeout determines how long to wait for a node
	// status RPC result.  If not provided (zero value), will default to 5 seconds.
	NodeCertificateStatusRequestTimeout time.Duration
	// RetryInterval specifies how long to delay between retries, if non-zero.
	RetryInterval time.Duration
	// Organization is the organization to use for a TLS certificate when creating
	// a security config from scratch.  If not provided, a random ID is generated.
	// For swarm certificates, the organization is the cluster ID.
	Organization string
}

// CreateSecurityConfig creates a new key and cert for this node, either locally
// or via a remote CA.
func (rootCA RootCA) CreateSecurityConfig(ctx context.Context, krw *KeyReadWriter, config CertificateRequestConfig) (*SecurityConfig, func() error, error) {
	ctx = log.WithModule(ctx, "tls")

	// Create a new random ID for this certificate
	cn := identity.NewID()
	org := config.Organization
	if config.Organization == "" {
		org = identity.NewID()
	}

	proposedRole := ManagerRole
	tlsKeyPair, issuerInfo, err := rootCA.IssueAndSaveNewCertificates(krw, cn, proposedRole, org)
	switch errors.Cause(err) {
	case ErrNoValidSigner:
		config.RetryInterval = GetCertRetryInterval
		// Request certificate issuance from a remote CA.
		// Last argument is nil because at this point we don't have any valid TLS creds
		tlsKeyPair, issuerInfo, err = rootCA.RequestAndSaveNewCertificates(ctx, krw, config)
		if err != nil {
			log.G(ctx).WithError(err).Error("failed to request and save new certificate")
			return nil, nil, err
		}
	case nil:
		log.G(ctx).WithFields(log.Fields{
			"node.id":   cn,
			"node.role": proposedRole,
		}).Debug("issued new TLS certificate")
	default:
		log.G(ctx).WithFields(log.Fields{
			"node.id":   cn,
			"node.role": proposedRole,
		}).WithError(err).Errorf("failed to issue and save new certificate")
		return nil, nil, err
	}

	secConfig, cleanup, err := NewSecurityConfig(&rootCA, krw, tlsKeyPair, issuerInfo)
	if err == nil {
		log.G(ctx).WithFields(log.Fields{
			"node.id":   secConfig.ClientTLSCreds.NodeID(),
			"node.role": secConfig.ClientTLSCreds.Role(),
		}).Debugf("new node credentials generated: %s", krw.Target())
	}
	return secConfig, cleanup, err
}

// TODO(cyli): currently we have to only update if it's a worker role - if we have a single root CA update path for
// both managers and workers, we won't need to check any more.
func updateRootThenUpdateCert(ctx context.Context, s *SecurityConfig, connBroker *connectionbroker.Broker, rootPaths CertPaths, failedCert *x509.Certificate) (*tls.Certificate, *IssuerInfo, error) {
	if len(failedCert.Subject.OrganizationalUnit) == 0 || failedCert.Subject.OrganizationalUnit[0] != WorkerRole {
		return nil, nil, errors.New("cannot update root CA since this is not a worker")
	}
	// try downloading a new root CA if it's an unknown authority issue, in case there was a root rotation completion
	// and we just didn't get the new root
	rootCA, err := GetRemoteCA(ctx, "", connBroker)
	if err != nil {
		return nil, nil, err
	}
	// validate against the existing security config creds
	if err := s.UpdateRootCA(&rootCA); err != nil {
		return nil, nil, err
	}
	if err := SaveRootCA(rootCA, rootPaths); err != nil {
		return nil, nil, err
	}
	return rootCA.RequestAndSaveNewCertificates(ctx, s.KeyWriter(),
		CertificateRequestConfig{
			ConnBroker:  connBroker,
			Credentials: s.ClientTLSCreds,
		})
}

// RenewTLSConfigNow gets a new TLS cert and key, and updates the security config if provided.  This is similar to
// RenewTLSConfig, except while that monitors for expiry, and periodically renews, this renews once and is blocking
func RenewTLSConfigNow(ctx context.Context, s *SecurityConfig, connBroker *connectionbroker.Broker, rootPaths CertPaths) error {
	s.renewalMu.Lock()
	defer s.renewalMu.Unlock()

	ctx = log.WithModule(ctx, "tls")
	logger := log.G(ctx).WithFields(log.Fields{
		"node.id":   s.ClientTLSCreds.NodeID(),
		"node.role": s.ClientTLSCreds.Role(),
	})

	// Let's request new certs. Renewals don't require a token.
	rootCA := s.RootCA()
	tlsKeyPair, issuerInfo, err := rootCA.RequestAndSaveNewCertificates(ctx,
		s.KeyWriter(),
		CertificateRequestConfig{
			ConnBroker:  connBroker,
			Credentials: s.ClientTLSCreds,
		})
	if wrappedError, ok := err.(x509UnknownAuthError); ok {
		var newErr error
		tlsKeyPair, issuerInfo, newErr = updateRootThenUpdateCert(ctx, s, connBroker, rootPaths, wrappedError.failedLeafCert)
		if newErr != nil {
			err = wrappedError.error
		} else {
			err = nil
		}
	}
	if err != nil {
		logger.WithError(err).Errorf("failed to renew the certificate")
		return err
	}

	return s.UpdateTLSCredentials(tlsKeyPair, issuerInfo)
}

// calculateRandomExpiry returns a random duration between 50% and 80% of the
// original validity period
func calculateRandomExpiry(validFrom, validUntil time.Time) time.Duration {
	duration := validUntil.Sub(validFrom)

	var randomExpiry int
	// Our lower bound of renewal will be half of the total expiration time
	minValidity := int(duration.Minutes() * CertLowerRotationRange)
	// Our upper bound of renewal will be 80% of the total expiration time
	maxValidity := int(duration.Minutes() * CertUpperRotationRange)
	// Let's select a random number of minutes between min and max, and set our retry for that
	// Using randomly selected rotation allows us to avoid certificate thundering herds.
	if maxValidity-minValidity < 1 {
		randomExpiry = minValidity
	} else {
		randomExpiry = rand.Intn(maxValidity-minValidity) + minValidity
	}

	expiry := time.Until(validFrom.Add(time.Duration(randomExpiry) * time.Minute))
	if expiry < 0 {
		return 0
	}
	return expiry
}

// NewServerTLSConfig returns a tls.Config configured for a TLS Server, given a tls.Certificate
// and the PEM-encoded root CA Certificate
func NewServerTLSConfig(certs []tls.Certificate, rootCAPool *x509.CertPool) (*tls.Config, error) {
	if rootCAPool == nil {
		return nil, errors.New("valid root CA pool required")
	}

	return &tls.Config{
		Certificates: certs,
		// Since we're using the same CA server to issue Certificates to new nodes, we can't
		// use tls.RequireAndVerifyClientCert
		ClientAuth:               tls.VerifyClientCertIfGiven,
		RootCAs:                  rootCAPool,
		ClientCAs:                rootCAPool,
		PreferServerCipherSuites: true,
		MinVersion:               tls.VersionTLS12,
		NextProtos:               alpnProtoStr,
	}, nil
}

// NewClientTLSConfig returns a tls.Config configured for a TLS Client, given a tls.Certificate
// the PEM-encoded root CA Certificate, and the name of the remote server the client wants to connect to.
func NewClientTLSConfig(certs []tls.Certificate, rootCAPool *x509.CertPool, serverName string) (*tls.Config, error) {
	if rootCAPool == nil {
		return nil, errors.New("valid root CA pool required")
	}

	return &tls.Config{
		ServerName:   serverName,
		Certificates: certs,
		RootCAs:      rootCAPool,
		MinVersion:   tls.VersionTLS12,
	}, nil
}

// NewClientTLSCredentials returns GRPC credentials for a TLS GRPC client, given a tls.Certificate
// a PEM-Encoded root CA Certificate, and the name of the remote server the client wants to connect to.
func (rootCA *RootCA) NewClientTLSCredentials(cert *tls.Certificate, serverName string) (*MutableTLSCreds, error) {
	tlsConfig, err := NewClientTLSConfig([]tls.Certificate{*cert}, rootCA.Pool, serverName)
	if err != nil {
		return nil, err
	}

	mtls, err := NewMutableTLS(tlsConfig)

	return mtls, err
}

// NewServerTLSCredentials returns GRPC credentials for a TLS GRPC client, given a tls.Certificate
// a PEM-Encoded root CA Certificate, and the name of the remote server the client wants to connect to.
func (rootCA *RootCA) NewServerTLSCredentials(cert *tls.Certificate) (*MutableTLSCreds, error) {
	tlsConfig, err := NewServerTLSConfig([]tls.Certificate{*cert}, rootCA.Pool)
	if err != nil {
		return nil, err
	}

	mtls, err := NewMutableTLS(tlsConfig)

	return mtls, err
}

// ParseRole parses an apiRole into an internal role string
func ParseRole(apiRole api.NodeRole) (string, error) {
	switch apiRole {
	case api.NodeRoleManager:
		return ManagerRole, nil
	case api.NodeRoleWorker:
		return WorkerRole, nil
	default:
		return "", errors.Errorf("failed to parse api role: %v", apiRole)
	}
}

// FormatRole parses an internal role string into an apiRole
func FormatRole(role string) (api.NodeRole, error) {
	switch strings.ToLower(role) {
	case strings.ToLower(ManagerRole):
		return api.NodeRoleManager, nil
	case strings.ToLower(WorkerRole):
		return api.NodeRoleWorker, nil
	default:
		return 0, errors.Errorf("failed to parse role: %s", role)
	}
}