// 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), } }