Files
oc-datacenter/infrastructure/minio/minio_setter.go

298 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package minio
import (
"context"
"encoding/json"
"fmt"
"slices"
"oc-datacenter/conf"
oclib "cloud.o-forge.io/core/oc-lib"
"cloud.o-forge.io/core/oc-lib/models/live"
"cloud.o-forge.io/core/oc-lib/tools"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// MinioCredentialEvent is the NATS payload used to transfer Minio credentials between peers.
//
// Two-phase protocol over PROPALGATION_EVENT (Action = PB_MINIO_CONFIG):
// - Phase 1 role assignment (Access == ""):
// oc-discovery routes this to the SOURCE peer (Minio host) → InitializeAsSource.
// - Phase 2 credential delivery (Access != ""):
// oc-discovery routes this to the TARGET peer (compute host) → InitializeAsTarget.
type MinioCredentialEvent struct {
ExecutionsID string `json:"executions_id"`
MinioID string `json:"minio_id"`
Access string `json:"access"`
Secret string `json:"secret"`
SourcePeerID string `json:"source_peer_id"`
DestPeerID string `json:"dest_peer_id"`
URL string `json:"url"`
// OriginID is the peer that initiated the provisioning request.
// The PB_CONSIDERS response is routed back to this peer.
OriginID string `json:"origin_id"`
}
// minioConsidersPayload is the PB_CONSIDERS payload emitted after minio provisioning.
type minioConsidersPayload struct {
OriginID string `json:"origin_id"`
ExecutionsID string `json:"executions_id"`
Secret string `json:"secret,omitempty"`
Error *string `json:"error,omitempty"`
}
// emitConsiders publishes a PB_CONSIDERS back to OriginID with the result of
// the minio provisioning. secret is the provisioned credential; err is nil on success.
func emitConsiders(executionsID, originID, secret string, provErr error) {
var errStr *string
if provErr != nil {
s := provErr.Error()
errStr = &s
}
payload, _ := json.Marshal(minioConsidersPayload{
OriginID: originID,
ExecutionsID: executionsID,
Secret: secret,
Error: errStr,
})
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,
})
}
// MinioSetter carries the execution context for a Minio credential provisioning.
type MinioSetter struct {
ExecutionsID string // used as both the bucket name and the K8s namespace suffix
MinioID string // ID of the Minio storage resource
}
func NewMinioSetter(execID, minioID string) *MinioSetter {
return &MinioSetter{ExecutionsID: execID, MinioID: minioID}
}
// InitializeAsSource is called on the peer that hosts the Minio instance.
//
// It:
// 1. Looks up the live-storage endpoint URL for MinioID.
// 2. Creates a scoped service account (access + secret limited to the execution bucket).
// 3. Creates the execution bucket.
// 4. If source and dest are the same peer, calls InitializeAsTarget directly.
// Otherwise, publishes a MinioCredentialEvent via NATS (Phase 2) so that
// oc-discovery can route the credentials to the compute peer.
func (m *MinioSetter) InitializeAsSource(ctx context.Context, localPeerID, destPeerID, originID string) {
logger := oclib.GetLogger()
url, err := m.loadMinioURL(localPeerID)
if err != nil {
logger.Error().Msg("MinioSetter.InitializeAsSource: " + err.Error())
return
}
service := NewMinioService(url)
if err := service.CreateClient(); err != nil {
logger.Error().Msg("MinioSetter.InitializeAsSource: failed to create admin client: " + err.Error())
return
}
access, secret, err := service.CreateCredentials(m.ExecutionsID)
if err != nil {
logger.Error().Msg("MinioSetter.InitializeAsSource: failed to create service account: " + err.Error())
return
}
if err := service.CreateBucket(m.MinioID, m.ExecutionsID); err != nil {
logger.Error().Msg("MinioSetter.InitializeAsSource: failed to create bucket: " + err.Error())
return
}
logger.Info().Msg("MinioSetter.InitializeAsSource: bucket and service account ready for " + m.ExecutionsID)
event := MinioCredentialEvent{
ExecutionsID: m.ExecutionsID,
MinioID: m.MinioID,
Access: access,
Secret: secret,
SourcePeerID: localPeerID,
DestPeerID: destPeerID,
OriginID: originID,
}
if destPeerID == localPeerID {
// Same peer: store the secret locally without going through NATS.
m.InitializeAsTarget(ctx, event)
return
}
// Cross-peer: publish credentials (Phase 2) so oc-discovery routes them to the compute peer.
payload, err := json.Marshal(event)
if err != nil {
logger.Error().Msg("MinioSetter.InitializeAsSource: failed to marshal credential event: " + err.Error())
return
}
if b, err := json.Marshal(&tools.PropalgationMessage{
DataType: -1,
Action: tools.PB_MINIO_CONFIG,
Payload: payload,
}); err == nil {
go tools.NewNATSCaller().SetNATSPub(tools.PROPALGATION_EVENT, tools.NATSResponse{
FromApp: "oc-datacenter",
Datatype: -1,
User: "",
Method: int(tools.PROPALGATION_EVENT),
Payload: b,
})
logger.Info().Msg("MinioSetter.InitializeAsSource: credentials published via NATS for " + m.ExecutionsID)
}
}
// InitializeAsTarget is called on the peer that runs the compute workload.
//
// It stores the Minio credentials received from the source peer (via NATS or directly)
// as a Kubernetes secret inside the execution namespace, making them available to pods.
func (m *MinioSetter) InitializeAsTarget(ctx context.Context, event MinioCredentialEvent) {
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("MinioSetter.InitializeAsTarget: failed to create k8s service: " + err.Error())
return
}
if err := k.CreateSecret(ctx, event.MinioID, event.ExecutionsID, event.Access, event.Secret); err != nil {
logger.Error().Msg("MinioSetter.InitializeAsTarget: failed to create k8s secret: " + err.Error())
emitConsiders(event.ExecutionsID, event.OriginID, "", err)
return
}
if err := NewMinioService(event.URL).CreateMinioConfigMap(event.MinioID, event.ExecutionsID, event.URL); err == nil {
logger.Error().Msg("MinioSetter.InitializeAsTarget: failed to create config map: " + err.Error())
emitConsiders(event.ExecutionsID, event.OriginID, "", err)
return
}
logger.Info().Msg("MinioSetter.InitializeAsTarget: Minio credentials stored in namespace " + event.ExecutionsID)
emitConsiders(event.ExecutionsID, event.OriginID, event.Secret, nil)
}
// MinioDeleteEvent is the NATS payload used to tear down Minio resources.
// It mirrors MinioCredentialEvent but carries the access key for revocation.
type MinioDeleteEvent struct {
ExecutionsID string `json:"executions_id"`
MinioID string `json:"minio_id"`
Access string `json:"access"` // service account access key to revoke on the Minio host
SourcePeerID string `json:"source_peer_id"`
DestPeerID string `json:"dest_peer_id"`
OriginID string `json:"origin_id"`
}
// TeardownAsTarget is called on the peer that runs the compute workload.
// It reads the stored access key from the K8s secret, then removes both the secret
// and the artifact-repository ConfigMap from the execution namespace.
// For same-peer deployments it calls TeardownAsSource directly; otherwise it
// publishes a MinioDeleteEvent via NATS (PB_DELETE) so oc-discovery routes it to
// the Minio host peer.
func (m *MinioSetter) TeardownAsTarget(ctx context.Context, event MinioDeleteEvent) {
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("MinioSetter.TeardownAsTarget: failed to create k8s service: " + err.Error())
emitConsiders(event.ExecutionsID, event.OriginID, "", err)
return
}
// Read the access key from the K8s secret before deleting it.
accessKey := event.Access
if accessKey == "" {
if secret, err := k.Set.CoreV1().Secrets(event.ExecutionsID).Get(
ctx, event.MinioID+"-secret-s3", metav1.GetOptions{},
); err == nil {
accessKey = string(secret.Data["access-key"])
}
}
// Delete K8s credentials secret.
if err := k.Set.CoreV1().Secrets(event.ExecutionsID).Delete(
ctx, event.MinioID+"-secret-s3", metav1.DeleteOptions{},
); err != nil {
logger.Error().Msg("MinioSetter.TeardownAsTarget: failed to delete secret: " + err.Error())
}
// Delete artifact-repository ConfigMap.
if err := NewMinioService("").DeleteMinioConfigMap(event.MinioID, event.ExecutionsID); err != nil {
logger.Error().Msg("MinioSetter.TeardownAsTarget: failed to delete configmap: " + err.Error())
}
logger.Info().Msg("MinioSetter.TeardownAsTarget: K8s resources removed for " + event.ExecutionsID)
// For same-peer deployments the source cleanup runs directly here so the
// caller (REMOVE_EXECUTION handler) doesn't have to distinguish roles.
if event.SourcePeerID == event.DestPeerID {
event.Access = accessKey
m.TeardownAsSource(ctx, event)
}
}
// TeardownAsSource is called on the peer that hosts the Minio instance.
// It revokes the scoped service account and removes the execution bucket.
func (m *MinioSetter) TeardownAsSource(ctx context.Context, event MinioDeleteEvent) {
logger := oclib.GetLogger()
url, err := m.loadMinioURL(event.SourcePeerID)
if err != nil {
logger.Error().Msg("MinioSetter.TeardownAsSource: " + err.Error())
return
}
svc := NewMinioService(url)
if err := svc.CreateClient(); err != nil {
logger.Error().Msg("MinioSetter.TeardownAsSource: failed to create admin client: " + err.Error())
return
}
if event.Access != "" {
if err := svc.DeleteCredentials(event.Access); err != nil {
logger.Error().Msg("MinioSetter.TeardownAsSource: failed to delete service account: " + err.Error())
}
}
if err := svc.DeleteBucket(event.MinioID, event.ExecutionsID); err != nil {
logger.Error().Msg("MinioSetter.TeardownAsSource: failed to delete bucket: " + err.Error())
}
logger.Info().Msg("MinioSetter.TeardownAsSource: Minio resources removed for " + event.ExecutionsID)
}
// loadMinioURL searches through all live storages accessible by peerID to find
// the one that references MinioID, and returns its endpoint URL.
func (m *MinioSetter) loadMinioURL(peerID string) (string, error) {
res := oclib.NewRequest(oclib.LibDataEnum(oclib.LIVE_STORAGE), "", peerID, []string{}, nil).LoadAll(false)
if res.Err != "" {
return "", fmt.Errorf("loadMinioURL: failed to load live storages: %s", res.Err)
}
for _, dbo := range res.Data {
l := dbo.(*live.LiveStorage)
if slices.Contains(l.ResourcesID, m.MinioID) {
return l.Source, nil
}
}
return "", fmt.Errorf("loadMinioURL: no live storage found for minio ID %s", m.MinioID)
}