package storage import ( "context" "encoding/json" "fmt" "slices" "strings" "oc-datacenter/conf" oclib "cloud.o-forge.io/core/oc-lib" "cloud.o-forge.io/core/oc-lib/dbs" bookingmodel "cloud.o-forge.io/core/oc-lib/models/booking" "cloud.o-forge.io/core/oc-lib/models/live" "cloud.o-forge.io/core/oc-lib/tools" ) // PVCProvisionEvent is the NATS payload for local PVC provisioning. // Same-peer deployments are handled directly; cross-peer routes via PB_PVC_CONFIG. type PVCProvisionEvent struct { ExecutionsID string `json:"executions_id"` StorageID string `json:"storage_id"` StorageName string `json:"storage_name"` SourcePeerID string `json:"source_peer_id"` DestPeerID string `json:"dest_peer_id"` OriginID string `json:"origin_id"` } // PVCDeleteEvent is the NATS payload for local PVC teardown. type PVCDeleteEvent struct { ExecutionsID string `json:"executions_id"` StorageID string `json:"storage_id"` StorageName string `json:"storage_name"` SourcePeerID string `json:"source_peer_id"` DestPeerID string `json:"dest_peer_id"` OriginID string `json:"origin_id"` } // ClaimName returns the deterministic PVC name shared by oc-datacenter and oc-monitord. func ClaimName(storageName, executionsID string) string { return strings.ReplaceAll(strings.ToLower(storageName), " ", "-") + "-" + executionsID } // PVCSetter carries the execution context for a local PVC provisioning. type PVCSetter struct { ExecutionsID string StorageID string // ClaimSuffix overrides ExecutionsID as the suffix in ClaimName when non-empty. // Used when the PVC namespace differs from the claim name suffix (Admiralty target). ClaimSuffix string } func NewPVCSetter(execID, storageID string) *PVCSetter { return &PVCSetter{ExecutionsID: execID, StorageID: storageID} } // NewPVCSetterWithClaimSuffix creates a PVCSetter where the claim name suffix // differs from the execution namespace (e.g. Admiralty target provisioning). func NewPVCSetterWithClaimSuffix(storageID, claimSuffix string) *PVCSetter { return &PVCSetter{StorageID: storageID, ClaimSuffix: claimSuffix} } func (p *PVCSetter) emitConsiders(executionsID, originID string, provErr error, self bool) { type pvcConsidersPayload struct { OriginID string `json:"origin_id"` ExecutionsID string `json:"executions_id"` Error *string `json:"error,omitempty"` } var errStr *string if provErr != nil { s := provErr.Error() errStr = &s } payload, _ := json.Marshal(pvcConsidersPayload{ OriginID: originID, ExecutionsID: executionsID, Error: errStr, }) if self { go tools.NewNATSCaller().SetNATSPub(tools.CONSIDERS_EVENT, tools.NATSResponse{ FromApp: "oc-datacenter", Datatype: tools.STORAGE_RESOURCE, Method: int(tools.CONSIDERS_EVENT), Payload: payload, }) return } b, _ := json.Marshal(&tools.PropalgationMessage{ DataType: tools.STORAGE_RESOURCE.EnumIndex(), Action: tools.PB_CONSIDERS, Payload: payload, }) go tools.NewNATSCaller().SetNATSPub(tools.PROPALGATION_EVENT, tools.NATSResponse{ FromApp: "oc-datacenter", Datatype: -1, Method: int(tools.PROPALGATION_EVENT), Payload: b, }) } // InitializeAsSource creates the PVC in the execution namespace on the local cluster. // self must be true when source and dest are the same peer (direct CONSIDERS_EVENT emission). func (p *PVCSetter) InitializeAsSource(ctx context.Context, event PVCProvisionEvent, self bool) { logger := oclib.GetLogger() sizeStr, err := p.loadStorageSize(event.SourcePeerID) if err != nil { logger.Error().Msg("PVCSetter.InitializeAsSource: " + err.Error()) p.emitConsiders(event.ExecutionsID, event.OriginID, err, self) return } k, err := tools.NewKubernetesService( conf.GetConfig().KubeHost+":"+conf.GetConfig().KubePort, conf.GetConfig().KubeCA, conf.GetConfig().KubeCert, conf.GetConfig().KubeData, ) if err != nil { logger.Error().Msg("PVCSetter.InitializeAsSource: failed to create k8s service: " + err.Error()) p.emitConsiders(event.ExecutionsID, event.OriginID, err, self) return } claimSuffix := event.ExecutionsID if p.ClaimSuffix != "" { claimSuffix = p.ClaimSuffix } claimName := ClaimName(event.StorageName, claimSuffix) if err := k.CreatePVC(ctx, claimName, event.ExecutionsID, sizeStr); err != nil { logger.Error().Msg("PVCSetter.InitializeAsSource: failed to create PVC: " + err.Error()) p.emitConsiders(event.ExecutionsID, event.OriginID, err, self) return } logger.Info().Msg("PVCSetter.InitializeAsSource: PVC " + claimName + " created in " + event.ExecutionsID) p.emitConsiders(event.ExecutionsID, event.OriginID, nil, self) } // TeardownAsSource deletes the PVC from the execution namespace. func (p *PVCSetter) TeardownAsSource(ctx context.Context, event PVCDeleteEvent) { logger := oclib.GetLogger() k, err := tools.NewKubernetesService( conf.GetConfig().KubeHost+":"+conf.GetConfig().KubePort, conf.GetConfig().KubeCA, conf.GetConfig().KubeCert, conf.GetConfig().KubeData, ) if err != nil { logger.Error().Msg("PVCSetter.TeardownAsSource: failed to create k8s service: " + err.Error()) return } claimName := ClaimName(event.StorageName, event.ExecutionsID) if err := k.DeletePVC(ctx, claimName, event.ExecutionsID); err != nil { logger.Error().Msg("PVCSetter.TeardownAsSource: failed to delete PVC: " + err.Error()) return } logger.Info().Msg("PVCSetter.TeardownAsSource: PVC " + claimName + " deleted from " + event.ExecutionsID) } // ResolveStorageName returns the live storage name for a given storageID, or "" if not found. func ResolveStorageName(storageID, peerID string) string { res := oclib.NewRequest(oclib.LibDataEnum(oclib.LIVE_STORAGE), "", peerID, []string{}, nil).LoadAll(false) if res.Err != "" { return "" } for _, dbo := range res.Data { l := dbo.(*live.LiveStorage) if slices.Contains(l.ResourcesID, storageID) { return l.GetName() } } return "" } // loadStorageSize looks up the SizeGB for this storage in live storages. func (p *PVCSetter) loadStorageSize(peerID string) (string, error) { res := oclib.NewRequest(oclib.LibDataEnum(oclib.LIVE_STORAGE), "", peerID, []string{}, nil).LoadAll(false) if res.Err != "" { return "", fmt.Errorf("loadStorageSize: %s", res.Err) } for _, dbo := range res.Data { l := dbo.(*live.LiveStorage) if slices.Contains(l.ResourcesID, p.StorageID) && l.SizeGB > 0 { return fmt.Sprintf("%dGi", l.SizeGB), nil } } return "10Gi", nil } // teardownPVCForExecution deletes all local PVCs provisioned for the execution. // It searches LIVE_STORAGE bookings and resolves the storage name via the live storage. func (p *PVCSetter) TeardownForExecution(ctx context.Context, localPeerID string) { logger := oclib.GetLogger() res := oclib.NewRequest(oclib.LibDataEnum(oclib.BOOKING), "", localPeerID, []string{}, nil). Search(&dbs.Filters{ And: map[string][]dbs.Filter{ "executions_id": {{Operator: dbs.EQUAL.String(), Value: p.ExecutionsID}}, "resource_type": {{Operator: dbs.EQUAL.String(), Value: tools.LIVE_STORAGE.EnumIndex()}}, }, }, "", false) if res.Err != "" || len(res.Data) == 0 { return } for _, dbo := range res.Data { b, ok := dbo.(*bookingmodel.Booking) if !ok { continue } // Resolve storage name from live storage to compute the claim name. storageName := ResolveStorageName(b.ResourceID, localPeerID) if storageName == "" { continue } logger.Info().Msgf("InfraTeardown: PVC teardown exec=%s storage=%s", p.ExecutionsID, b.ResourceID) event := PVCDeleteEvent{ ExecutionsID: p.ExecutionsID, StorageID: b.ResourceID, StorageName: storageName, SourcePeerID: localPeerID, DestPeerID: b.DestPeerID, OriginID: "", } p.StorageID = b.ResourceID p.TeardownAsSource(ctx, event) } }