github.com/kubeshop/testkube@v1.17.23/pkg/logs/adapter/minio.go (about)

     1  package adapter
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"strconv"
     9  	"sync"
    10  
    11  	"github.com/minio/minio-go/v7"
    12  	"go.uber.org/zap"
    13  
    14  	"github.com/kubeshop/testkube/pkg/log"
    15  	"github.com/kubeshop/testkube/pkg/logs/events"
    16  	minioconnecter "github.com/kubeshop/testkube/pkg/storage/minio"
    17  )
    18  
    19  const (
    20  	defaultBufferSize = 1024 * 100 // 100KB
    21  	defaultWriteSize  = 1024 * 80  // 80KB
    22  )
    23  
    24  var _ Adapter = &MinioAdapter{}
    25  
    26  type ErrMinioAdapterDisconnected struct {
    27  }
    28  
    29  func (e ErrMinioAdapterDisconnected) Error() string {
    30  	return "minio consumer disconnected"
    31  }
    32  
    33  type ErrIdNotFound struct {
    34  	Id string
    35  }
    36  
    37  func (e ErrIdNotFound) Error() string {
    38  	return fmt.Sprintf("id %s not found", e.Id)
    39  }
    40  
    41  type ErrChunckTooBig struct {
    42  	Length int
    43  }
    44  
    45  func (e ErrChunckTooBig) Error() string {
    46  	return fmt.Sprintf("chunk too big: %d", e.Length)
    47  }
    48  
    49  type BufferInfo struct {
    50  	Buffer *bytes.Buffer
    51  	Part   int
    52  }
    53  
    54  // NewMinioAdapter creates new MinioAdapter which will send data to local MinIO bucket
    55  func NewMinioAdapter(endpoint, accessKeyID, secretAccessKey, region, token, bucket string, ssl, skipVerify bool, certFile, keyFile, caFile string) (*MinioAdapter, error) {
    56  	ctx := context.TODO()
    57  	opts := minioconnecter.GetTLSOptions(ssl, skipVerify, certFile, keyFile, caFile)
    58  	c := &MinioAdapter{
    59  		minioConnecter: minioconnecter.NewConnecter(endpoint, accessKeyID, secretAccessKey, region, token, bucket, log.DefaultLogger, opts...),
    60  		Log:            log.DefaultLogger,
    61  		bucket:         bucket,
    62  		region:         region,
    63  		disconnected:   false,
    64  		buffInfos:      make(map[string]BufferInfo),
    65  	}
    66  	minioClient, err := c.minioConnecter.GetClient()
    67  	if err != nil {
    68  		c.Log.Errorw("error connecting to minio", "err", err)
    69  		return c, err
    70  	}
    71  
    72  	c.minioClient = minioClient
    73  	exists, err := c.minioClient.BucketExists(ctx, c.bucket)
    74  	if err != nil {
    75  		c.Log.Errorw("error checking if bucket exists", "err", err)
    76  		return c, err
    77  	}
    78  
    79  	if !exists {
    80  		err = c.minioClient.MakeBucket(ctx, c.bucket,
    81  			minio.MakeBucketOptions{Region: c.region})
    82  		if err != nil {
    83  			c.Log.Errorw("error creating bucket", "err", err)
    84  			return c, err
    85  		}
    86  	}
    87  	return c, nil
    88  }
    89  
    90  type MinioAdapter struct {
    91  	minioConnecter *minioconnecter.Connecter
    92  	minioClient    *minio.Client
    93  	bucket         string
    94  	region         string
    95  	Log            *zap.SugaredLogger
    96  	disconnected   bool
    97  	buffInfos      map[string]BufferInfo
    98  	mapLock        sync.RWMutex
    99  	traceMessages  bool
   100  }
   101  
   102  func (s *MinioAdapter) Init(ctx context.Context, id string) error {
   103  	return nil
   104  }
   105  
   106  func (s *MinioAdapter) WithTraceMessages(enabled bool) {
   107  	s.traceMessages = enabled
   108  }
   109  
   110  func (s *MinioAdapter) Notify(ctx context.Context, id string, e events.Log) error {
   111  	if s.traceMessages {
   112  		s.Log.Debugw("minio consumer notify", "id", id, "event", e)
   113  	}
   114  	if s.disconnected {
   115  		s.Log.Debugw("minio consumer disconnected", "id", id)
   116  		return ErrMinioAdapterDisconnected{}
   117  	}
   118  
   119  	buffInfo, ok := s.GetBuffInfo(id)
   120  	if !ok {
   121  		buffInfo = BufferInfo{Buffer: bytes.NewBuffer(make([]byte, 0, defaultBufferSize)), Part: 0}
   122  		s.UpdateBuffInfo(id, buffInfo)
   123  	}
   124  
   125  	chunckToAdd, err := json.Marshal(e)
   126  	if err != nil {
   127  		return err
   128  	}
   129  
   130  	if len(chunckToAdd) > defaultWriteSize {
   131  		s.Log.Warnw("chunck too big", "length", len(chunckToAdd))
   132  		return ErrChunckTooBig{len(chunckToAdd)}
   133  	}
   134  
   135  	chunckToAdd = append(chunckToAdd, []byte("\n")...)
   136  
   137  	writer := buffInfo.Buffer
   138  	_, err = writer.Write(chunckToAdd)
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	if writer.Len() > defaultWriteSize {
   144  		buffInfo.Buffer = bytes.NewBuffer(make([]byte, 0, defaultBufferSize))
   145  		name := id + "-" + strconv.Itoa(buffInfo.Part)
   146  		buffInfo.Part++
   147  		s.UpdateBuffInfo(id, buffInfo)
   148  		go s.putData(context.TODO(), name, writer)
   149  	}
   150  
   151  	return nil
   152  }
   153  
   154  func (s *MinioAdapter) putData(ctx context.Context, name string, buffer *bytes.Buffer) {
   155  	if buffer != nil && buffer.Len() != 0 {
   156  		_, err := s.minioClient.PutObject(ctx, s.bucket, name, buffer, int64(buffer.Len()), minio.PutObjectOptions{ContentType: "application/octet-stream"})
   157  		if err != nil {
   158  			s.Log.Errorw("error putting object", "err", err)
   159  		}
   160  		s.Log.Debugw("put object successfully", "name", name, "s.bucket", s.bucket)
   161  	} else {
   162  		s.Log.Warn("empty buffer for name: ", name)
   163  	}
   164  
   165  }
   166  
   167  func (s *MinioAdapter) combineData(ctxt context.Context, minioClient *minio.Client, id string, parts int, deleteIntermediaryData bool) error {
   168  	var returnedError []error
   169  	returnedError = nil
   170  	buffer := bytes.NewBuffer(make([]byte, 0, parts*defaultBufferSize))
   171  	for i := 0; i < parts; i++ {
   172  		objectName := fmt.Sprintf("%s-%d", id, i)
   173  		if s.objectExists(objectName) {
   174  			objInfo, err := minioClient.GetObject(ctxt, s.bucket, objectName, minio.GetObjectOptions{})
   175  			if err != nil {
   176  				s.Log.Errorw("error getting object", "err", err)
   177  				returnedError = append(returnedError, err)
   178  			}
   179  			_, err = buffer.ReadFrom(objInfo)
   180  			if err != nil {
   181  				s.Log.Errorw("error reading object", "err", err)
   182  				returnedError = append(returnedError, err)
   183  			}
   184  		}
   185  	}
   186  
   187  	info, err := minioClient.PutObject(ctxt, s.bucket, id, buffer, int64(buffer.Len()), minio.PutObjectOptions{ContentType: "application/octet-stream"})
   188  	if err != nil {
   189  		s.Log.Errorw("error putting object", "err", err)
   190  		return err
   191  	}
   192  	s.Log.Debugw("put object successfully", "id", id, "s.bucket", s.bucket, "parts", parts, "uploadInfo", info)
   193  
   194  	if deleteIntermediaryData {
   195  		for i := 0; i < parts; i++ {
   196  			objectName := fmt.Sprintf("%s-%d", id, i)
   197  			if s.objectExists(objectName) {
   198  				err = minioClient.RemoveObject(ctxt, s.bucket, objectName, minio.RemoveObjectOptions{})
   199  				if err != nil {
   200  					s.Log.Errorw("error removing object", "err", err)
   201  					returnedError = append(returnedError, err)
   202  				}
   203  			}
   204  		}
   205  	}
   206  
   207  	buffer.Reset()
   208  	if len(returnedError) == 0 {
   209  		return nil
   210  	}
   211  	return fmt.Errorf("executed with errors: %v", returnedError)
   212  }
   213  
   214  func (s *MinioAdapter) objectExists(objectName string) bool {
   215  	_, err := s.minioClient.StatObject(context.Background(), s.bucket, objectName, minio.StatObjectOptions{})
   216  	return err == nil
   217  }
   218  
   219  func (s *MinioAdapter) Stop(ctx context.Context, id string) error {
   220  	s.Log.Debugw("minio consumer stop", "id", id)
   221  	buffInfo, ok := s.GetBuffInfo(id)
   222  	if !ok {
   223  		return ErrIdNotFound{id}
   224  	}
   225  	name := id + "-" + strconv.Itoa(buffInfo.Part)
   226  	s.putData(ctx, name, buffInfo.Buffer)
   227  	parts := buffInfo.Part + 1
   228  	s.DeleteBuffInfo(id)
   229  	return s.combineData(ctx, s.minioClient, id, parts, true)
   230  }
   231  
   232  func (s *MinioAdapter) Name() string {
   233  	return "minio"
   234  }
   235  
   236  func (s *MinioAdapter) GetBuffInfo(id string) (BufferInfo, bool) {
   237  	s.mapLock.RLock()
   238  	defer s.mapLock.RUnlock()
   239  	buffInfo, ok := s.buffInfos[id]
   240  	return buffInfo, ok
   241  }
   242  
   243  func (s *MinioAdapter) UpdateBuffInfo(id string, buffInfo BufferInfo) {
   244  	s.mapLock.Lock()
   245  	defer s.mapLock.Unlock()
   246  	s.buffInfos[id] = buffInfo
   247  	s.Log.Debugw("minioAdapter: updated buff info", "id", id, "bufInfosCount", len(s.buffInfos))
   248  }
   249  
   250  func (s *MinioAdapter) DeleteBuffInfo(id string) {
   251  	s.mapLock.Lock()
   252  	defer s.mapLock.Unlock()
   253  	delete(s.buffInfos, id)
   254  	s.Log.Debugw("minioAdapter: deleted buff info", "id", id, "bufInfosCount", len(s.buffInfos))
   255  }