190 lines
5.4 KiB
Go
190 lines
5.4 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package csimanager
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/nomad/client/dynamicplugins"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/hashicorp/nomad/plugins/csi"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
type pluginFingerprinter struct {
|
|
logger hclog.Logger
|
|
client csi.CSIPlugin
|
|
info *dynamicplugins.PluginInfo
|
|
|
|
// basicInfo holds a cache of data that should not change within a CSI plugin.
|
|
// This allows us to minimize the number of requests we make to plugins on each
|
|
// run of the fingerprinter, and reduces the chances of performing overly
|
|
// expensive actions repeatedly, and improves stability of data through
|
|
// transient failures.
|
|
basicInfo *structs.CSIInfo
|
|
|
|
fingerprintNode bool
|
|
fingerprintController bool
|
|
|
|
hadFirstSuccessfulFingerprint bool
|
|
// hadFirstSuccessfulFingerprintCh is closed the first time a fingerprint
|
|
// is completed successfully.
|
|
hadFirstSuccessfulFingerprintCh chan struct{}
|
|
|
|
// requiresStaging is set on a first successful fingerprint. It allows the
|
|
// csimanager to efficiently query this as it shouldn't change after a plugin
|
|
// is started. Removing this bool will require storing a cache of recent successful
|
|
// results that can be used by subscribers of the `hadFirstSuccessfulFingerprintCh`.
|
|
requiresStaging bool
|
|
}
|
|
|
|
func (p *pluginFingerprinter) fingerprint(ctx context.Context) *structs.CSIInfo {
|
|
if p.basicInfo == nil {
|
|
info, err := p.buildBasicFingerprint(ctx)
|
|
if err != nil {
|
|
// If we receive a fingerprinting error, update the stats with as much
|
|
// info as possible and wait for the next fingerprint interval.
|
|
info.HealthDescription = fmt.Sprintf("failed initial fingerprint with err: %v", err)
|
|
info.Healthy = false
|
|
|
|
return info
|
|
}
|
|
|
|
// If fingerprinting succeeded, we don't need to repopulate the basic
|
|
// info again.
|
|
p.basicInfo = info
|
|
}
|
|
|
|
info := p.basicInfo.Copy()
|
|
var fp *structs.CSIInfo
|
|
var err error
|
|
|
|
if p.fingerprintNode {
|
|
fp, err = p.buildNodeFingerprint(ctx, info)
|
|
} else if p.fingerprintController {
|
|
fp, err = p.buildControllerFingerprint(ctx, info)
|
|
}
|
|
|
|
if err != nil {
|
|
info.Healthy = false
|
|
info.HealthDescription = fmt.Sprintf("failed fingerprinting with error: %v", err)
|
|
} else {
|
|
info = fp
|
|
if !p.hadFirstSuccessfulFingerprint {
|
|
p.hadFirstSuccessfulFingerprint = true
|
|
if p.fingerprintNode {
|
|
p.requiresStaging = info.NodeInfo.RequiresNodeStageVolume
|
|
}
|
|
close(p.hadFirstSuccessfulFingerprintCh)
|
|
}
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
func (p *pluginFingerprinter) buildBasicFingerprint(ctx context.Context) (*structs.CSIInfo, error) {
|
|
info := &structs.CSIInfo{
|
|
PluginID: p.info.Name,
|
|
AllocID: p.info.AllocID,
|
|
Provider: p.info.Options["Provider"],
|
|
ProviderVersion: p.info.Version,
|
|
Healthy: false,
|
|
HealthDescription: "initial fingerprint not completed",
|
|
}
|
|
|
|
if p.fingerprintNode {
|
|
info.NodeInfo = &structs.CSINodeInfo{}
|
|
}
|
|
if p.fingerprintController {
|
|
info.ControllerInfo = &structs.CSIControllerInfo{}
|
|
}
|
|
|
|
capabilities, err := p.client.PluginGetCapabilities(ctx)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
|
|
info.RequiresControllerPlugin = capabilities.HasControllerService()
|
|
info.RequiresTopologies = capabilities.HasToplogies()
|
|
|
|
if p.fingerprintNode {
|
|
nodeInfo, err := p.client.NodeGetInfo(ctx)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
|
|
info.NodeInfo.ID = nodeInfo.NodeID
|
|
info.NodeInfo.MaxVolumes = nodeInfo.MaxVolumes
|
|
info.NodeInfo.AccessibleTopology = structCSITopologyFromCSITopology(nodeInfo.AccessibleTopology)
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func applyCapabilitySetToControllerInfo(cs *csi.ControllerCapabilitySet, info *structs.CSIControllerInfo) {
|
|
info.SupportsCreateDelete = cs.HasCreateDeleteVolume
|
|
info.SupportsAttachDetach = cs.HasPublishUnpublishVolume
|
|
info.SupportsListVolumes = cs.HasListVolumes
|
|
info.SupportsGetCapacity = cs.HasGetCapacity
|
|
info.SupportsCreateDeleteSnapshot = cs.HasCreateDeleteSnapshot
|
|
info.SupportsListSnapshots = cs.HasListSnapshots
|
|
info.SupportsClone = cs.HasCloneVolume
|
|
info.SupportsReadOnlyAttach = cs.HasPublishReadonly
|
|
info.SupportsExpand = cs.HasExpandVolume
|
|
info.SupportsListVolumesAttachedNodes = cs.HasListVolumesPublishedNodes
|
|
info.SupportsCondition = cs.HasVolumeCondition
|
|
info.SupportsGet = cs.HasGetVolume
|
|
}
|
|
|
|
func (p *pluginFingerprinter) buildControllerFingerprint(ctx context.Context, base *structs.CSIInfo) (*structs.CSIInfo, error) {
|
|
fp := base.Copy()
|
|
|
|
healthy, err := p.client.PluginProbe(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fp.SetHealthy(healthy)
|
|
|
|
caps, err := p.client.ControllerGetCapabilities(ctx)
|
|
if err != nil {
|
|
return fp, err
|
|
}
|
|
applyCapabilitySetToControllerInfo(caps, fp.ControllerInfo)
|
|
|
|
return fp, nil
|
|
}
|
|
|
|
func (p *pluginFingerprinter) buildNodeFingerprint(ctx context.Context, base *structs.CSIInfo) (*structs.CSIInfo, error) {
|
|
fp := base.Copy()
|
|
|
|
healthy, err := p.client.PluginProbe(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fp.SetHealthy(healthy)
|
|
|
|
caps, err := p.client.NodeGetCapabilities(ctx)
|
|
if err != nil {
|
|
return fp, err
|
|
}
|
|
fp.NodeInfo.RequiresNodeStageVolume = caps.HasStageUnstageVolume
|
|
fp.NodeInfo.SupportsStats = caps.HasGetVolumeStats
|
|
fp.NodeInfo.SupportsExpand = caps.HasExpandVolume
|
|
fp.NodeInfo.SupportsCondition = caps.HasVolumeCondition
|
|
|
|
return fp, nil
|
|
}
|
|
|
|
func structCSITopologyFromCSITopology(a *csi.Topology) *structs.CSITopology {
|
|
if a == nil {
|
|
return nil
|
|
}
|
|
|
|
return &structs.CSITopology{
|
|
Segments: maps.Clone(a.Segments),
|
|
}
|
|
}
|