diff --git a/e2e/bittwister/suite_setup_test.go b/e2e/bittwister/suite_setup_test.go index 782e41ba..213fe883 100644 --- a/e2e/bittwister/suite_setup_test.go +++ b/e2e/bittwister/suite_setup_test.go @@ -16,11 +16,12 @@ type Suite struct { } func (s *Suite) SetupSuite() { - var ( - err error - ctx = context.Background() - ) - s.Knuu, err = knuu.New(ctx, knuu.Options{ProxyEnabled: true}) + ctx := context.Background() + + var err error + s.Knuu, err = knuu.New(ctx, knuu.Options{ + ProxyEnabled: true, + }) s.Require().NoError(err) s.T().Logf("Scope: %s", s.Knuu.Scope()) s.Knuu.HandleStopSignal(ctx) diff --git a/e2e/system/build_from_git_test.go b/e2e/system/build_from_git_test.go index 0df16300..03975f39 100644 --- a/e2e/system/build_from_git_test.go +++ b/e2e/system/build_from_git_test.go @@ -2,13 +2,17 @@ package system import ( "context" + "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/celestiaorg/knuu/pkg/builder" "github.com/celestiaorg/knuu/pkg/instance" + "github.com/celestiaorg/knuu/pkg/k8s" "github.com/celestiaorg/knuu/pkg/knuu" + "github.com/celestiaorg/knuu/pkg/minio" ) func TestBuildFromGit(t *testing.T) { @@ -21,6 +25,65 @@ func TestBuildFromGit(t *testing.T) { kn, err := knuu.New(ctx, knuu.Options{}) require.NoError(t, err, "Error creating knuu") + target, err := kn.NewInstance("git-builder") + require.NoError(t, err, "Error creating instance") + + t.Log("Building the image") + + // This is a blocking call which builds the image from git repo + err = target.SetGitRepo(ctx, builder.GitContext{ + Repo: "https://github.com/celestiaorg/knuu.git", + Branch: "test/build-from-git", // This branch has a Dockerfile and is protected as to not be deleted + Username: "", + Password: "", + }) + require.NoError(t, err, "Error setting git repo") + + t.Log("Image built") + + t.Cleanup(func() { + if err := target.Destroy(ctx); err != nil { + t.Logf("Error destroying instance: %v", err) + } + }) + + require.NoError(t, target.Commit()) + + t.Logf("Starting instance") + + assert.NoError(t, target.Start(ctx)) + + t.Logf("Instance started") + + // The file is created by the dockerfile in the repo, + // so to make sure it is built correctly, we check the file + data, err := target.GetFileBytes(ctx, "/test.txt") + require.NoError(t, err, "Error getting file bytes") + + data = []byte(strings.TrimSpace(string(data))) + assert.Equal(t, []byte("Hello, World!"), data, "File bytes do not match") +} +func TestBuildFromGitWithModifications(t *testing.T) { + t.Parallel() + + // Setup + ctx := context.Background() + + k8sClient, err := k8s.NewClient(ctx, knuu.DefaultTestScope()) + require.NoError(t, err, "Error creating k8s client") + + // Since we are modifying the git repo, + // we need to setup minio to allow the builder to push the changes + minioClient, err := minio.New(ctx, k8sClient) + require.NoError(t, err, "Error creating minio client") + + // The default image builder is kaniko here + kn, err := knuu.New(ctx, knuu.Options{ + K8sClient: k8sClient, + MinioClient: minioClient, + }) + require.NoError(t, err, "Error creating knuu") + sampleInstance, err := kn.NewInstance("git-builder") require.NoError(t, err, "Error creating instance") diff --git a/e2e/tshark/tshark_test.go b/e2e/tshark/tshark_test.go index f82b9279..0d834f5d 100644 --- a/e2e/tshark/tshark_test.go +++ b/e2e/tshark/tshark_test.go @@ -12,7 +12,9 @@ import ( "github.com/stretchr/testify/require" "github.com/celestiaorg/knuu/pkg/instance" + "github.com/celestiaorg/knuu/pkg/k8s" "github.com/celestiaorg/knuu/pkg/knuu" + "github.com/celestiaorg/knuu/pkg/minio" ) const ( @@ -26,7 +28,13 @@ func TestTshark(t *testing.T) { ctx := context.Background() - kn, err := knuu.New(ctx, knuu.Options{}) + k8sClient, err := k8s.NewClient(ctx, knuu.DefaultTestScope()) + require.NoError(t, err, "error creating k8s client") + + minioClient, err := minio.New(ctx, k8sClient) + require.NoError(t, err, "error creating minio client") + + kn, err := knuu.New(ctx, knuu.Options{MinioClient: minioClient, K8sClient: k8sClient}) require.NoError(t, err, "error creating knuu") defer func() { @@ -47,10 +55,6 @@ func TestTshark(t *testing.T) { err = target.SetCommand("sleep", "infinity") require.NoError(t, err, "error setting command") - t.Log("deploying minio as s3 backend") - err = kn.MinioClient.DeployMinio(ctx) - require.NoError(t, err, "error deploying minio") - t.Log("getting minio configs") minioConf, err := kn.MinioClient.GetConfigs(ctx) require.NoError(t, err, "error getting S3 (minio) configs") @@ -93,7 +97,7 @@ func TestTshark(t *testing.T) { _, err = target.ExecuteCommand(ctx, "ping", "-c", "4", "google.com") require.NoError(t, err, "error executing command") - url, err := kn.MinioClient.GetMinioURL(ctx, fileKey, s3BucketName) + url, err := kn.MinioClient.GetURL(ctx, fileKey, s3BucketName) require.NoError(t, err, "error getting minio url") resp, err := http.Get(url) diff --git a/pkg/builder/kaniko/errors.go b/pkg/builder/kaniko/errors.go index f993ef1a..2505f5f1 100644 --- a/pkg/builder/kaniko/errors.go +++ b/pkg/builder/kaniko/errors.go @@ -30,4 +30,5 @@ var ( ErrMinioDeploymentFailed = errors.New("MinioDeploymentFailed", "Minio deployment failed") ErrDeletingMinioContent = errors.New("DeletingMinioContent", "error deleting Minio content") ErrParsingQuantity = errors.New("ParsingQuantity", "error parsing quantity") + ErrMinioFailedToGetDeployment = errors.New("MinioFailedToGetDeployment", "Minio failed to get deployment") ) diff --git a/pkg/builder/kaniko/kaniko.go b/pkg/builder/kaniko/kaniko.go index 721707c2..b2adaa17 100644 --- a/pkg/builder/kaniko/kaniko.go +++ b/pkg/builder/kaniko/kaniko.go @@ -13,9 +13,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/celestiaorg/knuu/pkg/builder" - "github.com/celestiaorg/knuu/pkg/k8s" - "github.com/celestiaorg/knuu/pkg/minio" "github.com/celestiaorg/knuu/pkg/names" + "github.com/celestiaorg/knuu/pkg/system" ) const ( @@ -31,9 +30,8 @@ const ( ) type Kaniko struct { - K8s k8s.KubeManager - Minio *minio.Minio // Minio service to store the build context if it's a directory - ContentName string // Name of the content pushed to Minio + system.SystemDependencies + ContentName string // Name of the content pushed to Minio } var _ builder.Builder = &Kaniko{} @@ -44,7 +42,7 @@ func (k *Kaniko) Build(ctx context.Context, b *builder.BuilderOptions) (logs str return "", ErrPreparingJob.Wrap(err) } - cJob, err := k.K8s.Clientset().BatchV1().Jobs(k.K8s.Namespace()).Create(ctx, job, metav1.CreateOptions{}) + cJob, err := k.K8sClient.Clientset().BatchV1().Jobs(k.K8sClient.Namespace()).Create(ctx, job, metav1.CreateOptions{}) if err != nil { return "", ErrCreatingJob.Wrap(err) } @@ -76,7 +74,7 @@ func (k *Kaniko) Build(ctx context.Context, b *builder.BuilderOptions) (logs str } func (k *Kaniko) waitForJobCompletion(ctx context.Context, job *batchv1.Job) (*batchv1.Job, error) { - watcher, err := k.K8s.Clientset().BatchV1().Jobs(k.K8s.Namespace()).Watch(ctx, metav1.ListOptions{ + watcher, err := k.K8sClient.Clientset().BatchV1().Jobs(k.K8sClient.Namespace()).Watch(ctx, metav1.ListOptions{ FieldSelector: fmt.Sprintf("metadata.name=%s", job.Name), }) if err != nil { @@ -107,7 +105,7 @@ func (k *Kaniko) waitForJobCompletion(ctx context.Context, job *batchv1.Job) (*b } func (k *Kaniko) firstPodFromJob(ctx context.Context, job *batchv1.Job) (*v1.Pod, error) { - podList, err := k.K8s.Clientset().CoreV1().Pods(k.K8s.Namespace()).List(ctx, metav1.ListOptions{ + podList, err := k.K8sClient.Clientset().CoreV1().Pods(k.K8sClient.Namespace()).List(ctx, metav1.ListOptions{ LabelSelector: fmt.Sprintf("job-name=%s", job.Name), }) if err != nil { @@ -130,7 +128,7 @@ func (k *Kaniko) containerLogs(ctx context.Context, pod *v1.Pod) (string, error) Container: pod.Spec.Containers[0].Name, } - req := k.K8s.Clientset().CoreV1().Pods(k.K8s.Namespace()).GetLogs(pod.Name, &logOptions) + req := k.K8sClient.Clientset().CoreV1().Pods(k.K8sClient.Namespace()).GetLogs(pod.Name, &logOptions) logs, err := req.DoRaw(ctx) if err != nil { return "", err @@ -140,7 +138,7 @@ func (k *Kaniko) containerLogs(ctx context.Context, pod *v1.Pod) (string, error) } func (k *Kaniko) cleanup(ctx context.Context, job *batchv1.Job) error { - err := k.K8s.Clientset().BatchV1().Jobs(k.K8s.Namespace()). + err := k.K8sClient.Clientset().BatchV1().Jobs(k.K8sClient.Namespace()). Delete(ctx, job.Name, metav1.DeleteOptions{ PropagationPolicy: &[]metav1.DeletionPropagation{metav1.DeletePropagationBackground}[0], }) @@ -149,7 +147,7 @@ func (k *Kaniko) cleanup(ctx context.Context, job *batchv1.Job) error { } // Delete the associated Pods - err = k.K8s.Clientset().CoreV1().Pods(k.K8s.Namespace()). + err = k.K8sClient.Clientset().CoreV1().Pods(k.K8sClient.Namespace()). DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ LabelSelector: fmt.Sprintf("job-name=%s", job.Name), }) @@ -159,7 +157,7 @@ func (k *Kaniko) cleanup(ctx context.Context, job *batchv1.Job) error { // Delete the content pushed to Minio if k.ContentName != "" { - if err := k.Minio.DeleteFromMinio(ctx, k.ContentName, MinioBucketName); err != nil { + if err := k.MinioClient.Delete(ctx, k.ContentName, MinioBucketName); err != nil { return ErrDeletingMinioContent.Wrap(err) } } @@ -248,10 +246,6 @@ func (k *Kaniko) prepareJob(ctx context.Context, b *builder.BuilderOptions) (*ba // As kaniko also supports directly tar.gz archives, no need to extract it, // we just need to set the context to tar:// func (k *Kaniko) mountDir(ctx context.Context, bCtx string, job *batchv1.Job) (*batchv1.Job, error) { - if k.Minio == nil { - return nil, ErrMinioNotConfigured - } - // Create the tar.gz archive archiveData, err := createTarGz(builder.GetDirFromBuildContext(bCtx)) if err != nil { @@ -263,15 +257,11 @@ func (k *Kaniko) mountDir(ctx context.Context, bCtx string, job *batchv1.Job) (* hash.Write(archiveData) k.ContentName = hex.EncodeToString(hash.Sum(nil)) - if err := k.Minio.DeployMinio(ctx); err != nil { - return nil, ErrMinioDeploymentFailed.Wrap(err) - } - - if err := k.Minio.PushToMinio(ctx, bytes.NewReader(archiveData), k.ContentName, MinioBucketName); err != nil { + if err := k.MinioClient.Push(ctx, bytes.NewReader(archiveData), k.ContentName, MinioBucketName); err != nil { return nil, err } - s3URL, err := k.Minio.GetMinioURL(ctx, k.ContentName, MinioBucketName) + s3URL, err := k.MinioClient.GetURL(ctx, k.ContentName, MinioBucketName) if err != nil { return nil, err } diff --git a/pkg/builder/kaniko/kaniko_test.go b/pkg/builder/kaniko/kaniko_test.go index e138ca9b..2f3dc87f 100644 --- a/pkg/builder/kaniko/kaniko_test.go +++ b/pkg/builder/kaniko/kaniko_test.go @@ -15,6 +15,7 @@ import ( "github.com/celestiaorg/knuu/pkg/builder" "github.com/celestiaorg/knuu/pkg/k8s" + "github.com/celestiaorg/knuu/pkg/system" ) const ( @@ -28,7 +29,9 @@ func TestKanikoBuilder(t *testing.T) { k8sClient, err := k8s.NewClientCustom(context.Background(), k8sCS, k8sCS.Discovery(), nil, k8sNamespace) require.NoError(t, err) kb := &Kaniko{ - K8s: k8sClient, + SystemDependencies: system.SystemDependencies{ + K8sClient: k8sClient, + }, } ctx := context.Background() diff --git a/pkg/knuu/errors.go b/pkg/knuu/errors.go index 489b58f4..d2be8969 100644 --- a/pkg/knuu/errors.go +++ b/pkg/knuu/errors.go @@ -205,4 +205,7 @@ var ( ErrCannotGetTraefikEndpoint = errors.New("CannotGetTraefikEndpoint", "cannot get traefik endpoint") ErrGettingProxyURL = errors.New("GettingProxyURL", "error getting proxy URL for service '%s'") ErrTraefikAPINotAvailable = errors.New("TraefikAPINotAvailable", "traefik API is not available") + ErrTestScopeNotSet = errors.New("TestScopeNotSet", "test scope is not set") + ErrK8sClientNotSet = errors.New("K8sClientNotSet", "k8s client is not set") + ErrTestScopeMistMatch = errors.New("TestScopeMistMatch", "test scope '%s' set in options does not match scope '%s' set by the k8sClient namespace") ) diff --git a/pkg/knuu/knuu.go b/pkg/knuu/knuu.go index f004d478..b960733c 100644 --- a/pkg/knuu/knuu.go +++ b/pkg/knuu/knuu.go @@ -39,24 +39,28 @@ type Knuu struct { } type Options struct { - K8s k8s.KubeManager - TestScope string + K8sClient k8s.KubeManager + MinioClient *minio.Minio ImageBuilder builder.Builder - Minio *minio.Minio - Timeout time.Duration + TestScope string ProxyEnabled bool + Timeout time.Duration Logger *logrus.Logger } func New(ctx context.Context, opts Options) (*Knuu, error) { + if err := validateOptions(opts); err != nil { + return nil, err + } + if err := loadEnvVariables(); err != nil { return nil, err } k := &Knuu{ SystemDependencies: system.SystemDependencies{ - K8sClient: opts.K8s, - MinioClient: opts.Minio, + K8sClient: opts.K8sClient, + MinioClient: opts.MinioClient, ImageBuilder: opts.ImageBuilder, Logger: opts.Logger, TestScope: opts.TestScope, @@ -160,6 +164,24 @@ func (k *Knuu) handleTimeout(ctx context.Context) error { return nil } +func DefaultTestScope() string { + t := time.Now() + return fmt.Sprintf("%s-%03d", t.Format("20060102-150405"), t.Nanosecond()/1e6) +} + +func validateOptions(opts Options) error { + // When Minio is set, K8sClient must be set too + // to make sure that there is only one source of truth for the k8s client + if opts.MinioClient != nil && opts.K8sClient == nil { + return ErrK8sClientNotSet + } + + if opts.TestScope != "" && opts.K8sClient != nil && opts.TestScope != opts.K8sClient.Namespace() { + return ErrTestScopeMistMatch.WithParams(opts.TestScope, opts.K8sClient.Namespace()) + } + return nil +} + func loadEnvVariables() error { err := godotenv.Load() if err != nil && !os.IsNotExist(err) { @@ -177,8 +199,11 @@ func setDefaults(ctx context.Context, k *Knuu) error { } if k.TestScope == "" { - t := time.Now() - k.TestScope = fmt.Sprintf("%s-%03d", t.Format("20060102-150405"), t.Nanosecond()/1e6) + if k.K8sClient != nil { + k.TestScope = k.K8sClient.Namespace() + } else { + k.TestScope = DefaultTestScope() + } } k.TestScope = k8s.SanitizeName(k.TestScope) @@ -194,16 +219,9 @@ func setDefaults(ctx context.Context, k *Knuu) error { } } - if k.MinioClient == nil { - k.MinioClient = &minio.Minio{ - K8s: k.K8sClient, - } - } - if k.ImageBuilder == nil { k.ImageBuilder = &kaniko.Kaniko{ - K8s: k.K8sClient, - Minio: k.MinioClient, + SystemDependencies: k.SystemDependencies, } } diff --git a/pkg/knuu/knuu_old.go b/pkg/knuu/knuu_old.go index f431fe61..5ebb1218 100644 --- a/pkg/knuu/knuu_old.go +++ b/pkg/knuu/knuu_old.go @@ -19,8 +19,12 @@ import ( "github.com/celestiaorg/knuu/pkg/builder" "github.com/celestiaorg/knuu/pkg/builder/docker" "github.com/celestiaorg/knuu/pkg/builder/kaniko" + "github.com/celestiaorg/knuu/pkg/k8s" + "github.com/celestiaorg/knuu/pkg/minio" ) +const minioBucketName = "knuu" + // This is a temporary variable to hold the knuu instance until we refactor knuu pkg // TODO: remove this temporary variable var tmpKnuu *Knuu @@ -68,11 +72,23 @@ func InitializeWithScope(testScope string) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - var err error + + k8sClient, err := k8s.NewClient(ctx, testScope) + if err != nil { + return ErrCannotInitializeKnuu.Wrap(err) + } + + minioClient, err := minio.New(ctx, k8sClient) + if err != nil { + return ErrCannotInitializeKnuu.Wrap(err) + } + tmpKnuu, err = New(ctx, Options{ + K8sClient: k8sClient, TestScope: testScope, Timeout: timeout, ProxyEnabled: true, + MinioClient: minioClient, }) if err != nil { return ErrCannotInitializeKnuu.Wrap(err) @@ -82,8 +98,7 @@ func InitializeWithScope(testScope string) error { switch builderType { case "kubernetes": tmpKnuu.ImageBuilder = &kaniko.Kaniko{ - K8s: tmpKnuu.K8sClient, - Minio: tmpKnuu.MinioClient, + SystemDependencies: tmpKnuu.SystemDependencies, } case "docker", "": tmpKnuu.ImageBuilder = &docker.Docker{ @@ -135,10 +150,10 @@ func CleanUp() error { // Deprecated: Use the new package knuu instead. func PushFileToMinio(ctx context.Context, contentName string, reader io.Reader) error { - return tmpKnuu.PushFileToMinio(ctx, contentName, reader) + return tmpKnuu.MinioClient.Push(ctx, reader, contentName, minioBucketName) } // Deprecated: Use the new package knuu instead. func GetMinioURL(ctx context.Context, contentName string) (string, error) { - return tmpKnuu.GetMinioURL(ctx, contentName) + return tmpKnuu.MinioClient.GetURL(ctx, contentName, minioBucketName) } diff --git a/pkg/knuu/knuu_test.go b/pkg/knuu/knuu_test.go index eb11633f..3e5b521d 100644 --- a/pkg/knuu/knuu_test.go +++ b/pkg/knuu/knuu_test.go @@ -59,77 +59,87 @@ func TestNew(t *testing.T) { defer cancel() tt := []struct { - name string - options Options - expectError bool - validateFunc func(*testing.T, *Knuu) + name string + options Options + expectedError error + validateFunc func(*testing.T, *Knuu) }{ { - name: "Default initialization", - options: Options{}, - expectError: false, + name: "Default initialization", + options: Options{}, + expectedError: nil, validateFunc: func(t *testing.T, k *Knuu) { assert.NotNil(t, k) assert.NotNil(t, k.Logger) assert.NotNil(t, k.K8sClient) - assert.NotNil(t, k.MinioClient) assert.NotNil(t, k.ImageBuilder) + assert.NotEmpty(t, k.TestScope) assert.Equal(t, defaultTimeout, k.timeout) }, }, { - name: "With custom Logger", + name: "With Minio client without setting k8sClient", + options: Options{MinioClient: &minio.Minio{}}, + expectedError: ErrK8sClientNotSet, + }, + { + name: "With Minio client and K8sClient", options: Options{ - Logger: &logrus.Logger{}, + MinioClient: &minio.Minio{}, + K8sClient: &mockK8s{}, }, - expectError: false, + expectedError: nil, validateFunc: func(t *testing.T, k *Knuu) { assert.NotNil(t, k) - assert.NotNil(t, k.Logger) + assert.NotNil(t, k.MinioClient) + assert.NotNil(t, k.K8sClient) }, }, { - name: "With custom Timeout", + name: "With custom Logger", options: Options{ - Timeout: 30 * time.Minute, + TestScope: "test", + Logger: &logrus.Logger{}, }, - expectError: false, + expectedError: nil, validateFunc: func(t *testing.T, k *Knuu) { assert.NotNil(t, k) - assert.Equal(t, 30*time.Minute, k.timeout) + assert.NotNil(t, k.Logger) }, }, { - name: "With custom K8s client", + name: "With custom Timeout", options: Options{ - K8s: &mockK8s{}, + TestScope: "test", + Timeout: 30 * time.Minute, }, - expectError: false, + expectedError: nil, validateFunc: func(t *testing.T, k *Knuu) { assert.NotNil(t, k) - assert.NotNil(t, k.K8sClient) + assert.Equal(t, 30*time.Minute, k.timeout) }, }, { - name: "With custom Minio client", + name: "With custom Image Builder", options: Options{ - Minio: &minio.Minio{}, + TestScope: "test", + ImageBuilder: &kaniko.Kaniko{}, }, - expectError: false, + expectedError: nil, validateFunc: func(t *testing.T, k *Knuu) { assert.NotNil(t, k) - assert.NotNil(t, k.MinioClient) + assert.NotNil(t, k.ImageBuilder) }, }, { - name: "With custom Image Builder", + name: "With K8sClient but without TestScope", options: Options{ - ImageBuilder: &kaniko.Kaniko{}, + K8sClient: &mockK8s{}, }, - expectError: false, + expectedError: nil, validateFunc: func(t *testing.T, k *Knuu) { assert.NotNil(t, k) - assert.NotNil(t, k.ImageBuilder) + assert.Equal(t, "test", k.TestScope) }, }, } @@ -137,8 +147,9 @@ func TestNew(t *testing.T) { for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { k, err := New(ctx, tc.options) - if tc.expectError { + if tc.expectedError != nil { assert.Error(t, err) + assert.ErrorIs(t, err, tc.expectedError) return } @@ -147,3 +158,59 @@ func TestNew(t *testing.T) { }) } } + +func TestValidateOptions(t *testing.T) { + tests := []struct { + name string + options Options + expectedErr error + }{ + { + name: "MinioClient set without K8sClient", + options: Options{ + MinioClient: &minio.Minio{}, + }, + expectedErr: ErrK8sClientNotSet, + }, + { + name: "Both MinioClient and K8sClient set", + options: Options{ + MinioClient: &minio.Minio{}, + K8sClient: &mockK8s{}, + }, + expectedErr: nil, + }, + { + name: "TestScope and K8sClient not set", + options: Options{ + TestScope: "", + K8sClient: nil, + }, + expectedErr: nil, + }, + { + name: "TestScope does not match K8sClient namespace", + options: Options{ + TestScope: "another_scope", + K8sClient: &mockK8s{}, + }, + expectedErr: ErrTestScopeMistMatch.WithParams("another_scope", "test"), + }, + { + name: "No options set", + options: Options{}, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateOptions(tt.options) + if tt.expectedErr != nil { + assert.ErrorIs(t, err, tt.expectedErr) + return + } + assert.NoError(t, err) + }) + } +} diff --git a/pkg/knuu/minio.go b/pkg/knuu/minio.go deleted file mode 100644 index 49f4169d..00000000 --- a/pkg/knuu/minio.go +++ /dev/null @@ -1,38 +0,0 @@ -package knuu - -import ( - "context" - "io" -) - -const minioBucketName = "knuu" - -func (k *Knuu) initMinio(ctx context.Context) error { - if k.MinioClient == nil { - return ErrMinioNotInitialized - } - - ok, err := k.MinioClient.IsMinioDeployed(ctx) - if err != nil { - return err - } - if ok { - return nil - } - return k.MinioClient.DeployMinio(ctx) -} - -// contentName is a unique string to identify the content in Minio -func (k *Knuu) PushFileToMinio(ctx context.Context, contentName string, reader io.Reader) error { - if err := k.initMinio(ctx); err != nil { - return err - } - return k.MinioClient.PushToMinio(ctx, reader, contentName, minioBucketName) -} - -func (k *Knuu) GetMinioURL(ctx context.Context, contentName string) (string, error) { - if err := k.initMinio(ctx); err != nil { - return "", err - } - return k.MinioClient.GetMinioURL(ctx, contentName, minioBucketName) -} diff --git a/pkg/minio/errors.go b/pkg/minio/errors.go index cb4e7ad6..fceb49e7 100644 --- a/pkg/minio/errors.go +++ b/pkg/minio/errors.go @@ -40,4 +40,6 @@ var ( ErrMinioFailedToListPersistentVolumes = errors.New("MinioFailedToListPersistentVolumes", "failed to list PersistentVolumes") ErrMinioFailedToCreatePersistentVolume = errors.New("MinioFailedToCreatePersistentVolume", "failed to create PersistentVolume") ErrMinioFailedToCreatePersistentVolumeClaim = errors.New("MinioFailedToCreatePersistentVolumeClaim", "failed to create PersistentVolumeClaim") + ErrMinioClientNotInitialized = errors.New("MinioClientNotInitialized", "Minio client not initialized") + ErrMinioNotInitialized = errors.New("MinioNotInitialized", "Minio not initialized") ) diff --git a/pkg/minio/minio.go b/pkg/minio/minio.go index 9fb1774e..7f3068f8 100644 --- a/pkg/minio/minio.go +++ b/pkg/minio/minio.go @@ -42,7 +42,8 @@ const ( ) type Minio struct { - K8s k8s.KubeManager + client *miniogo.Client + k8sClient k8s.KubeManager } type Config struct { @@ -51,7 +52,107 @@ type Config struct { SecretAccessKey string } -func (m *Minio) DeployMinio(ctx context.Context) error { +func New(ctx context.Context, k8sClient k8s.KubeManager) (*Minio, error) { + m := &Minio{ + k8sClient: k8sClient, + } + + if err := m.deployMinio(ctx); err != nil { + return nil, err + } + + endpoint, err := m.getEndpoint(ctx) + if err != nil { + return nil, ErrMinioFailedToGetEndpoint.Wrap(err) + } + + m.client, err = miniogo.New(endpoint, &miniogo.Options{ + Creds: credentials.NewStaticV4(rootUser, rootPassword, ""), + Secure: false, + }) + if err != nil { + return nil, ErrMinioFailedToInitializeClient.Wrap(err) + } + + return m, nil +} + +// Push pushes data (i.e. a reader) to Minio +func (m *Minio) Push(ctx context.Context, localReader io.Reader, minioFilePath, bucketName string) error { + if m == nil { + return ErrMinioNotInitialized + } + + if err := m.createBucketIfNotExists(ctx, bucketName); err != nil { + return ErrMinioFailedToCreateBucket.Wrap(err) + } + + uploadInfo, err := m.client.PutObject(ctx, bucketName, minioFilePath, localReader, -1, miniogo.PutObjectOptions{}) + if err != nil { + return ErrMinioFailedToUploadData.Wrap(err) + } + + logrus.Debugf("Data uploaded successfully to %s in bucket %s", uploadInfo.Key, bucketName) + return nil +} + +// Delete deletes a file from Minio and fails if the content does not exist +func (m *Minio) Delete(ctx context.Context, minioFilePath, bucketName string) error { + if m == nil { + return ErrMinioNotInitialized + } + + // Check if the object exists before attempting to delete + _, err := m.client.StatObject(ctx, bucketName, minioFilePath, miniogo.StatObjectOptions{}) + if err != nil { + return ErrMinioFailedToFindFileBeforeDeletion.Wrap(err) + } + + err = m.client.RemoveObject(ctx, bucketName, minioFilePath, miniogo.RemoveObjectOptions{}) + if err != nil { + return ErrMinioFailedToDeleteFile.Wrap(err) + } + + logrus.Debugf("File %s deleted successfully from bucket %s", minioFilePath, bucketName) + return nil +} + +// GetURL returns an S3-compatible URL for a Minio file +func (m *Minio) GetURL(ctx context.Context, minioFilePath, bucketName string) (string, error) { + if m == nil { + return "", ErrMinioNotInitialized + } + + // Set the expiration time for the URL (e.g., 24h from now) + expiration := 24 * time.Hour + + // Generate a presigned URL for the object + presignedURL, err := m.client.PresignedGetObject(ctx, bucketName, minioFilePath, expiration, nil) + if err != nil { + return "", ErrMinioFailedToGeneratePresignedURL.Wrap(err) + } + + return presignedURL.String(), nil +} + +func (m *Minio) GetConfigs(ctx context.Context) (*Config, error) { + if m == nil { + return nil, ErrMinioNotInitialized + } + + endpoint, err := m.getEndpoint(ctx) + if err != nil { + return nil, ErrMinioFailedToGetEndpoint.Wrap(err) + } + + return &Config{ + Endpoint: endpoint, + AccessKeyID: rootUser, + SecretAccessKey: rootPassword, + }, nil +} + +func (m *Minio) deployMinio(ctx context.Context) error { if err := m.createOrUpdateDeployment(ctx); err != nil { return ErrMinioFailedToStart.Wrap(err) } @@ -64,7 +165,7 @@ func (m *Minio) DeployMinio(ctx context.Context) error { return ErrMinioFailedToCreateOrUpdateService.Wrap(err) } - if err := m.K8s.WaitForService(ctx, ServiceName); err != nil { + if err := m.k8sClient.WaitForService(ctx, ServiceName); err != nil { return ErrMinioFailedToBeReadyService.Wrap(err) } @@ -73,13 +174,13 @@ func (m *Minio) DeployMinio(ctx context.Context) error { } func (m *Minio) createOrUpdateDeployment(ctx context.Context) error { - deploymentClient := m.K8s.Clientset().AppsV1().Deployments(m.K8s.Namespace()) + deploymentClient := m.k8sClient.Clientset().AppsV1().Deployments(m.k8sClient.Namespace()) // Define the Minio deployment minioDeployment := &appsV1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: DeploymentName, - Namespace: m.K8s.Namespace(), + Namespace: m.k8sClient.Namespace(), }, Spec: appsV1.DeploymentSpec{ Selector: &metav1.LabelSelector{ @@ -151,126 +252,14 @@ func (m *Minio) createOrUpdateDeployment(ctx context.Context) error { return nil } -func (m *Minio) IsMinioDeployed(ctx context.Context) (bool, error) { - deploymentClient := m.K8s.Clientset().AppsV1().Deployments(m.K8s.Namespace()) - - _, err := deploymentClient.Get(ctx, DeploymentName, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return false, nil - } - return false, ErrMinioFailedToGetService.Wrap(err) - } - - return true, nil -} - -// PushToMinio pushes data (i.e. a reader) to Minio -func (m *Minio) PushToMinio(ctx context.Context, localReader io.Reader, minioFilePath, bucketName string) error { - endpoint, err := m.getEndpoint(ctx) - if err != nil { - return ErrMinioFailedToGetEndpoint.Wrap(err) - } - - cli, err := miniogo.New(endpoint, &miniogo.Options{ - Creds: credentials.NewStaticV4(rootUser, rootPassword, ""), - Secure: false, - }) - if err != nil { - return ErrMinioFailedToInitializeClient.Wrap(err) - } - - if err := m.createBucketIfNotExists(ctx, cli, bucketName); err != nil { - return ErrMinioFailedToCreateBucket.Wrap(err) - } - - uploadInfo, err := cli.PutObject(ctx, bucketName, minioFilePath, localReader, -1, miniogo.PutObjectOptions{}) - if err != nil { - return ErrMinioFailedToUploadData.Wrap(err) - } - - logrus.Debugf("Data uploaded successfully to %s in bucket %s", uploadInfo.Key, bucketName) - return nil -} - -// DeleteFromMinio deletes a file from Minio and fails if the content does not exist -func (m *Minio) DeleteFromMinio(ctx context.Context, minioFilePath, bucketName string) error { - endpoint, err := m.getEndpoint(ctx) - if err != nil { - return ErrMinioFailedToGetPresignedURL.Wrap(err) - } - - cli, err := miniogo.New(endpoint, &miniogo.Options{ - Creds: credentials.NewStaticV4(rootUser, rootPassword, ""), - Secure: false, - }) - if err != nil { - return ErrMinioFailedToUpdateService.Wrap(err) - } - - // Check if the object exists before attempting to delete - _, err = cli.StatObject(ctx, bucketName, minioFilePath, miniogo.StatObjectOptions{}) - if err != nil { - return ErrMinioFailedToFindFileBeforeDeletion.Wrap(err) - } - - err = cli.RemoveObject(ctx, bucketName, minioFilePath, miniogo.RemoveObjectOptions{}) - if err != nil { - return ErrMinioFailedToDeleteFile.Wrap(err) - } - - logrus.Debugf("File %s deleted successfully from bucket %s", minioFilePath, bucketName) - return nil -} - -// GetMinioURL returns an S3-compatible URL for a Minio file -func (m *Minio) GetMinioURL(ctx context.Context, minioFilePath, bucketName string) (string, error) { - minioEndpoint, err := m.getEndpoint(ctx) - if err != nil { - return "", ErrMinioFailedToGetMinioEndpoint.Wrap(err) - } - // Initialize Minio client - minioClient, err := miniogo.New(minioEndpoint, &miniogo.Options{ - Creds: credentials.NewStaticV4(rootUser, rootPassword, ""), - Secure: false, - }) - if err != nil { - return "", ErrMinioFailedToInitializeClient.Wrap(err) - } - - // Set the expiration time for the URL (e.g., 24h from now) - expiration := 24 * time.Hour - - // Generate a presigned URL for the object - presignedURL, err := minioClient.PresignedGetObject(ctx, bucketName, minioFilePath, expiration, nil) - if err != nil { - return "", ErrMinioFailedToGeneratePresignedURL.Wrap(err) - } - - return presignedURL.String(), nil -} - -func (m *Minio) GetConfigs(ctx context.Context) (*Config, error) { - endpoint, err := m.getEndpoint(ctx) - if err != nil { - return nil, ErrMinioFailedToGetEndpoint.Wrap(err) - } - - return &Config{ - Endpoint: endpoint, - AccessKeyID: rootUser, - SecretAccessKey: rootPassword, - }, nil -} - func (m *Minio) createOrUpdateService(ctx context.Context) error { - serviceClient := m.K8s.Clientset().CoreV1().Services(m.K8s.Namespace()) + serviceClient := m.k8sClient.Clientset().CoreV1().Services(m.k8sClient.Namespace()) // Define Minio service minioService := &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: ServiceName, - Namespace: m.K8s.Namespace(), + Namespace: m.k8sClient.Namespace(), }, Spec: v1.ServiceSpec{ Selector: map[string]string{"app": "minio"}, @@ -313,8 +302,12 @@ func (m *Minio) createOrUpdateService(ctx context.Context) error { return nil } -func (m *Minio) createBucketIfNotExists(ctx context.Context, cli *miniogo.Client, bucketName string) error { - exists, err := cli.BucketExists(ctx, bucketName) +func (m *Minio) createBucketIfNotExists(ctx context.Context, bucketName string) error { + if m.client == nil { + return ErrMinioClientNotInitialized + } + + exists, err := m.client.BucketExists(ctx, bucketName) if err != nil { return ErrMinioFailedToCheckBucket.Wrap(err) } @@ -322,7 +315,7 @@ func (m *Minio) createBucketIfNotExists(ctx context.Context, cli *miniogo.Client return nil } - if err := cli.MakeBucket(ctx, bucketName, miniogo.MakeBucketOptions{}); err != nil { + if err := m.client.MakeBucket(ctx, bucketName, miniogo.MakeBucketOptions{}); err != nil { return ErrMinioFailedToCreateBucket.Wrap(err) } logrus.Debugf("Bucket `%s` created successfully.", bucketName) @@ -331,7 +324,7 @@ func (m *Minio) createBucketIfNotExists(ctx context.Context, cli *miniogo.Client } func (m *Minio) getEndpoint(ctx context.Context) (string, error) { - minioService, err := m.K8s.Clientset().CoreV1().Services(m.K8s.Namespace()).Get(ctx, ServiceName, metav1.GetOptions{}) + minioService, err := m.k8sClient.Clientset().CoreV1().Services(m.k8sClient.Namespace()).Get(ctx, ServiceName, metav1.GetOptions{}) if err != nil { return "", ErrMinioFailedToGetService.Wrap(err) } @@ -346,7 +339,7 @@ func (m *Minio) getEndpoint(ctx context.Context) (string, error) { if minioService.Spec.Type == v1.ServiceTypeNodePort { // Use the Node IP and NodePort - nodes, err := m.K8s.Clientset().CoreV1().Nodes().List(ctx, metav1.ListOptions{}) + nodes, err := m.k8sClient.Clientset().CoreV1().Nodes().List(ctx, metav1.ListOptions{}) if err != nil { return "", ErrMinioFailedToGetNodes.Wrap(err) } @@ -370,7 +363,7 @@ func (m *Minio) getEndpoint(ctx context.Context) (string, error) { func (m *Minio) waitForMinio(ctx context.Context) error { for { - deployment, err := m.K8s.Clientset().AppsV1().Deployments(m.K8s.Namespace()).Get(ctx, DeploymentName, metav1.GetOptions{}) + deployment, err := m.k8sClient.Clientset().AppsV1().Deployments(m.k8sClient.Namespace()).Get(ctx, DeploymentName, metav1.GetOptions{}) if err == nil && deployment.Status.ReadyReplicas > 0 { break } @@ -392,7 +385,7 @@ func (m *Minio) createPVC(ctx context.Context, pvcName string, storageSize strin return ErrMinioFailedToParseStorageSize.Wrap(err) } - pvcClient := m.K8s.Clientset().CoreV1().PersistentVolumeClaims(m.K8s.Namespace()) + pvcClient := m.k8sClient.Clientset().CoreV1().PersistentVolumeClaims(m.k8sClient.Namespace()) // Check if PVC already exists _, err = pvcClient.Get(ctx, pvcName, metav1.GetOptions{}) @@ -402,7 +395,7 @@ func (m *Minio) createPVC(ctx context.Context, pvcName string, storageSize strin } // Create a simple PersistentVolume if no suitable one is found - pvList, err := m.K8s.Clientset().CoreV1().PersistentVolumes().List(ctx, metav1.ListOptions{}) + pvList, err := m.k8sClient.Clientset().CoreV1().PersistentVolumes().List(ctx, metav1.ListOptions{}) if err != nil { return ErrMinioFailedToListPersistentVolumes.Wrap(err) } @@ -418,7 +411,7 @@ func (m *Minio) createPVC(ctx context.Context, pvcName string, storageSize strin if existingPV == nil { // Create a simple PV if no existing PV is suitable - _, err = m.K8s.Clientset().CoreV1().PersistentVolumes().Create(ctx, &v1.PersistentVolume{ + _, err = m.k8sClient.Clientset().CoreV1().PersistentVolumes().Create(ctx, &v1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ GenerateName: pvPrefix, }, @@ -444,7 +437,7 @@ func (m *Minio) createPVC(ctx context.Context, pvcName string, storageSize strin pvc := &v1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: pvcName, - Namespace: m.K8s.Namespace(), + Namespace: m.k8sClient.Namespace(), }, Spec: v1.PersistentVolumeClaimSpec{ AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},