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 }