diff --git a/pkg/controller/job-controller.go b/pkg/controller/job-controller.go
index 728df52e9f8..b43afa7d23e 100644
--- a/pkg/controller/job-controller.go
+++ b/pkg/controller/job-controller.go
@@ -1,11 +1,27 @@
// This file is part of MinIO Operator
// Copyright (c) 2024 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
package controller
import (
"context"
"fmt"
+ "reflect"
+ "strings"
+ "sync"
"time"
"github.com/minio/minio-go/v7/pkg/set"
@@ -14,7 +30,10 @@ import (
stsv1alpha1 "github.com/minio/operator/pkg/apis/sts.min.io/v1alpha1"
jobinformers "github.com/minio/operator/pkg/client/informers/externalversions/job.min.io/v1alpha1"
joblisters "github.com/minio/operator/pkg/client/listers/job.min.io/v1alpha1"
+ runtime2 "github.com/minio/operator/pkg/runtime"
+ "github.com/minio/operator/pkg/utils/miniojob"
batchjobv1 "k8s.io/api/batch/v1"
+ corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -29,6 +48,34 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
)
+const (
+ commandFilePath = "/temp"
+ minioJobName = "job.min.io/job-name"
+ minioJobCRName = "job.min.io/job-cr-name"
+ // DefaultMCImage - job mc image
+ DefaultMCImage = "minio/mc:latest"
+ // MinioJobPhaseError - error
+ MinioJobPhaseError = "Error"
+ // MinioJobPhaseSuccess - success
+ MinioJobPhaseSuccess = "Success"
+ // MinioJobPhaseRunning - running
+ MinioJobPhaseRunning = "Running"
+ // MinioJobPhaseFailed - failed
+ MinioJobPhaseFailed = "Failed"
+)
+
+var operationAlias = map[string]string{
+ "make-bucket": "mb",
+ "admin/policy/add": "admin/policy/create",
+}
+
+var jobOperation = map[string][]miniojob.FieldsFunc{
+ "mb": {miniojob.FLAGS(), miniojob.Sanitize(miniojob.ALIAS(), miniojob.Static("/"), miniojob.Key("name")), miniojob.Static("--ignore-existing")},
+ "admin/user/add": {miniojob.ALIAS(), miniojob.Key("user"), miniojob.Key("password")},
+ "admin/policy/create": {miniojob.ALIAS(), miniojob.Key("name"), miniojob.File("policy", "json")},
+ "admin/policy/attach": {miniojob.ALIAS(), miniojob.Key("policy"), miniojob.OneOf(miniojob.KeyForamt("user", "--user"), miniojob.KeyForamt("group", "--group"))},
+}
+
// JobController struct watches the Kubernetes API for changes to Tenant resources
type JobController struct {
namespacesToWatch set.StringSet
@@ -107,12 +154,33 @@ func NewJobController(
jobInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
UpdateFunc: func(old, new interface{}) {
- oldJob := old.(*batchjobv1.Job)
newJob := new.(*batchjobv1.Job)
- if oldJob.ResourceVersion == newJob.ResourceVersion {
+ jobName, ok := newJob.Labels[minioJobName]
+ if !ok {
+ return
+ }
+ jobCRName, ok := newJob.Labels[minioJobCRName]
+ if !ok {
return
}
- // todo record the job status.
+ val, ok := globalIntervalJobStatus.Load(fmt.Sprintf("%s/%s", newJob.GetNamespace(), jobCRName))
+ if ok {
+ intervalJob := val.(*MinIOIntervalJob)
+ command, ok := intervalJob.CommandMap[jobName]
+ if ok {
+ if newJob.Status.Succeeded > 0 {
+ command.setStatus(true, "")
+ } else {
+ for _, condition := range newJob.Status.Conditions {
+ if condition.Type == batchjobv1.JobFailed {
+ command.setStatus(false, condition.Message)
+ break
+ }
+ }
+ }
+ }
+ }
+ controller.HandleObject(newJob)
},
})
return controller
@@ -163,11 +231,22 @@ func (c *JobController) SyncHandler(key string) (Result, error) {
err := c.k8sClient.Get(ctx, client.ObjectKeyFromObject(&jobCR), &jobCR)
if err != nil {
// job cr have gone
+ globalIntervalJobStatus.Delete(fmt.Sprintf("%s/%s", jobCR.Namespace, jobCR.Name))
if errors.IsNotFound(err) {
return WrapResult(Result{}, nil)
}
return WrapResult(Result{}, err)
}
+ // if job cr is success, do nothing
+ if jobCR.Status.Phase == MinioJobPhaseSuccess {
+ // delete the job status
+ globalIntervalJobStatus.Delete(fmt.Sprintf("%s/%s", jobCR.Namespace, jobCR.Name))
+ return WrapResult(Result{}, nil)
+ }
+ intervalJob, err := checkMinIOJob(&jobCR)
+ if err != nil {
+ return WrapResult(Result{}, err)
+ }
// get tenant
tenant := &miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
@@ -177,7 +256,7 @@ func (c *JobController) SyncHandler(key string) (Result, error) {
}
err = c.k8sClient.Get(ctx, client.ObjectKeyFromObject(tenant), tenant)
if err != nil {
- jobCR.Status.Phase = "Error"
+ jobCR.Status.Phase = MinioJobPhaseError
jobCR.Status.Message = fmt.Sprintf("Get tenant %s/%s error:%v", jobCR.Spec.TenantRef.Namespace, jobCR.Spec.TenantRef.Name, err)
err = c.updateJobStatus(ctx, &jobCR)
return WrapResult(Result{}, err)
@@ -203,16 +282,375 @@ func (c *JobController) SyncHandler(key string) (Result, error) {
if !saFound {
return WrapResult(Result{}, fmt.Errorf("no serviceaccount found"))
}
- // Loop through the different supported operations.
- for _, val := range jobCR.Spec.Commands {
- operation := val.Operation
- if operation == "make-bucket" {
- // TODO: Initiate a job to create the bucket(s) if they do not exist and if the Tenant is prepared for it.
- }
+ err = intervalJob.createCommandJob(ctx, c.k8sClient)
+ if err != nil {
+ jobCR.Status.Phase = MinioJobPhaseError
+ jobCR.Status.Message = fmt.Sprintf("Create job error:%v", err)
+ err = c.updateJobStatus(ctx, &jobCR)
+ return WrapResult(Result{}, err)
}
+ // update status
+ jobCR.Status = intervalJob.getMinioJobStatus(ctx)
+ err = c.updateJobStatus(ctx, &jobCR)
return WrapResult(Result{}, err)
}
func (c *JobController) updateJobStatus(ctx context.Context, job *v1alpha1.MinIOJob) error {
return c.k8sClient.Status().Update(ctx, job)
}
+
+func operationAliasToMC(operation string) (op string, found bool) {
+ for k, v := range operationAlias {
+ if k == operation {
+ return v, true
+ }
+ if v == operation {
+ return v, true
+ }
+ }
+ // operation like admin/policy/attach match nothing.
+ // but it's a valid operation
+ if strings.Contains(operation, "/") {
+ return operation, true
+ }
+ // operation like replace match nothing
+ // it's not a valid operation
+ return "", false
+}
+
+// MinIOIntervalJobCommandFile - Job run command need a file such as /temp/policy.json
+type MinIOIntervalJobCommandFile struct {
+ Name string
+ Ext string
+ Dir string
+ Content string
+}
+
+// MinIOIntervalJobCommand - Job run command
+type MinIOIntervalJobCommand struct {
+ mutex sync.RWMutex
+ JobName string
+ MCOperation string
+ Command string
+ DepnedsOn []string
+ Files []MinIOIntervalJobCommandFile
+ Succeeded bool
+ Message string
+ Created bool
+}
+
+func (jobCommand *MinIOIntervalJobCommand) setStatus(success bool, message string) {
+ if jobCommand == nil {
+ return
+ }
+ jobCommand.mutex.Lock()
+ jobCommand.Succeeded = success
+ jobCommand.Message = message
+ jobCommand.mutex.Unlock()
+}
+
+func (jobCommand *MinIOIntervalJobCommand) success() bool {
+ if jobCommand == nil {
+ return false
+ }
+ jobCommand.mutex.Lock()
+ defer jobCommand.mutex.Unlock()
+ return jobCommand.Succeeded
+}
+
+func (jobCommand *MinIOIntervalJobCommand) createJob(ctx context.Context, k8sClient client.Client, jobCR *v1alpha1.MinIOJob) error {
+ if jobCommand == nil {
+ return nil
+ }
+ jobCommand.mutex.RLock()
+ if jobCommand.Created || jobCommand.Succeeded {
+ jobCommand.mutex.RUnlock()
+ return nil
+ }
+ jobCommand.mutex.RUnlock()
+ jobCommands := []string{}
+ commands := []string{"mc"}
+ commands = append(commands, strings.SplitN(jobCommand.MCOperation, "/", -1)...)
+ commands = append(commands, strings.SplitN(jobCommand.Command, " ", -1)...)
+ for _, command := range commands {
+ trimCommand := strings.TrimSpace(command)
+ if trimCommand != "" {
+ jobCommands = append(jobCommands, trimCommand)
+ }
+ }
+ jobCommands = append(jobCommands, "--insecure")
+ objs := []client.Object{}
+ mcImage := jobCR.Spec.MCImage
+ if mcImage == "" {
+ mcImage = DefaultMCImage
+ }
+ job := &batchjobv1.Job{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("%s-%s", jobCR.Name, jobCommand.JobName),
+ Namespace: jobCR.Namespace,
+ Labels: map[string]string{
+ minioJobName: jobCommand.JobName,
+ minioJobCRName: jobCR.Name,
+ },
+ Annotations: map[string]string{
+ "job.min.io/operation": jobCommand.MCOperation,
+ },
+ },
+ Spec: batchjobv1.JobSpec{
+ Template: corev1.PodTemplateSpec{
+ ObjectMeta: metav1.ObjectMeta{
+ Labels: map[string]string{
+ minioJobName: jobCommand.JobName,
+ },
+ },
+ Spec: corev1.PodSpec{
+ ServiceAccountName: jobCR.Spec.ServiceAccountName,
+ Containers: []corev1.Container{
+ {
+ Name: "mc",
+ Image: mcImage,
+ ImagePullPolicy: corev1.PullIfNotPresent,
+ Env: []corev1.EnvVar{
+ {
+ Name: "MC_HOST_myminio",
+ Value: fmt.Sprintf("https://$(ACCESS_KEY):$(SECRET_KEY)@minio.%s.svc.cluster.local", jobCR.Namespace),
+ },
+ {
+ Name: "MC_STS_ENDPOINT_myminio",
+ Value: fmt.Sprintf("https://sts.%s.svc.cluster.local:4223/sts/%s", miniov2.GetNSFromFile(), jobCR.Namespace),
+ },
+ {
+ Name: "MC_WEB_IDENTITY_TOKEN_FILE_myminio",
+ Value: "/var/run/secrets/kubernetes.io/serviceaccount/token",
+ },
+ },
+ Command: jobCommands,
+ },
+ },
+ },
+ },
+ },
+ }
+ if jobCR.Spec.FailureStrategy == v1alpha1.StopOnFailure {
+ job.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicyNever
+ } else {
+ job.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicyOnFailure
+ }
+ if len(jobCommand.Files) > 0 {
+ cmName := fmt.Sprintf("%s-%s-cm", jobCR.Name, jobCommand.JobName)
+ job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
+ Name: "file-volume",
+ ReadOnly: true,
+ MountPath: jobCommand.Files[0].Dir,
+ })
+ job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
+ Name: "file-volume",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: cmName,
+ },
+ },
+ },
+ })
+ configMap := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: cmName,
+ Namespace: jobCR.Namespace,
+ Labels: map[string]string{
+ "job.min.io/name": jobCR.Name,
+ },
+ },
+ Data: map[string]string{},
+ }
+ for _, file := range jobCommand.Files {
+ configMap.Data[fmt.Sprintf("%s.%s", file.Name, file.Ext)] = file.Content
+ }
+ objs = append(objs, configMap)
+ }
+ objs = append(objs, job)
+ for _, obj := range objs {
+ _, err := runtime2.NewObjectSyncer(ctx, k8sClient, jobCR, func() error {
+ return nil
+ }, obj, runtime2.SyncTypeCreateOrUpdate).Sync(ctx)
+ if err != nil {
+ return err
+ }
+ }
+ jobCommand.mutex.Lock()
+ jobCommand.Created = true
+ jobCommand.mutex.Unlock()
+ return nil
+}
+
+// MinIOIntervalJob - Interval Job
+type MinIOIntervalJob struct {
+ // to see if that change
+ JobCR *v1alpha1.MinIOJob
+ Command []*MinIOIntervalJobCommand
+ CommandMap map[string]*MinIOIntervalJobCommand
+}
+
+func (intervalJob *MinIOIntervalJob) getMinioJobStatus(ctx context.Context) v1alpha1.MinIOJobStatus {
+ status := v1alpha1.MinIOJobStatus{}
+ failed := false
+ running := false
+ message := ""
+ for _, command := range intervalJob.Command {
+ command.mutex.RLock()
+ if command.Succeeded {
+ status.CommandsStatus = append(status.CommandsStatus, v1alpha1.CommandStatus{
+ Name: command.JobName,
+ Result: "success",
+ Message: command.Message,
+ })
+ } else {
+ failed = true
+ message = command.Message
+ // if success is false and message is empty, it means the job is running
+ if command.Message == "" {
+ running = true
+ status.CommandsStatus = append(status.CommandsStatus, v1alpha1.CommandStatus{
+ Name: command.JobName,
+ Result: "running",
+ Message: command.Message,
+ })
+ } else {
+ status.CommandsStatus = append(status.CommandsStatus, v1alpha1.CommandStatus{
+ Name: command.JobName,
+ Result: "failed",
+ Message: command.Message,
+ })
+ }
+ }
+ command.mutex.RUnlock()
+ }
+ if running {
+ status.Phase = MinioJobPhaseRunning
+ } else {
+ if failed {
+ status.Phase = MinioJobPhaseFailed
+ status.Message = message
+ } else {
+ status.Phase = MinioJobPhaseSuccess
+ }
+ }
+ return status
+}
+
+func (intervalJob *MinIOIntervalJob) createCommandJob(ctx context.Context, k8sClient client.Client) error {
+ for _, command := range intervalJob.Command {
+ if len(command.DepnedsOn) == 0 {
+ err := command.createJob(ctx, k8sClient, intervalJob.JobCR)
+ if err != nil {
+ return err
+ }
+ } else {
+ allDepsSuccess := true
+ for _, dep := range command.DepnedsOn {
+ status, found := intervalJob.CommandMap[dep]
+ if !found {
+ return fmt.Errorf("dependent job %s not found", dep)
+ }
+ if !status.success() {
+ allDepsSuccess = false
+ break
+ }
+ }
+ if allDepsSuccess {
+ err := command.createJob(ctx, k8sClient, intervalJob.JobCR)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func checkMinIOJob(jobCR *v1alpha1.MinIOJob) (intervalJob *MinIOIntervalJob, err error) {
+ defer func() {
+ if err != nil {
+ globalIntervalJobStatus.Delete(fmt.Sprintf("%s/%s", jobCR.Namespace, jobCR.Name))
+ }
+ }()
+ val, found := globalIntervalJobStatus.Load(fmt.Sprintf("%s/%s", jobCR.Namespace, jobCR.Name))
+ if found {
+ intervalJob = val.(*MinIOIntervalJob)
+ if reflect.DeepEqual(intervalJob.JobCR.Spec, jobCR.Spec) {
+ intervalJob.JobCR.UID = jobCR.UID
+ return intervalJob, nil
+ }
+ }
+ intervalJob = &MinIOIntervalJob{
+ JobCR: jobCR.DeepCopy(),
+ Command: []*MinIOIntervalJobCommand{},
+ CommandMap: map[string]*MinIOIntervalJobCommand{},
+ }
+ if jobCR.Spec.TenantRef.Namespace == "" {
+ return intervalJob, fmt.Errorf("tenant namespace is empty")
+ }
+ if jobCR.Spec.TenantRef.Name == "" {
+ return intervalJob, fmt.Errorf("tenant name is empty")
+ }
+ if jobCR.Spec.ServiceAccountName == "" {
+ return intervalJob, fmt.Errorf("serviceaccount name is empty")
+ }
+ for index, val := range jobCR.Spec.Commands {
+ mcCommand, found := operationAliasToMC(val.Operation)
+ if !found {
+ return intervalJob, fmt.Errorf("operation %s is not supported", val.Operation)
+ }
+ commands := []string{}
+ files := []MinIOIntervalJobCommandFile{}
+ argsFuncs, found := jobOperation[mcCommand]
+ if !found {
+ return intervalJob, fmt.Errorf("operation %s is not supported", mcCommand)
+ }
+ for _, argsFunc := range argsFuncs {
+ jobArg, err := argsFunc(val.Args)
+ if err != nil {
+ return intervalJob, err
+ }
+ if jobArg.IsFile() {
+ files = append(files, MinIOIntervalJobCommandFile{
+ Name: jobArg.FileName,
+ Ext: jobArg.FileExt,
+ Dir: commandFilePath,
+ Content: jobArg.FileContext,
+ })
+ commands = append(commands, fmt.Sprintf("%s/%s.%s", commandFilePath, jobArg.FileName, jobArg.FileExt))
+ } else {
+ if jobArg.Command != "" {
+ commands = append(commands, jobArg.Command)
+ }
+ }
+ }
+ jobCommand := MinIOIntervalJobCommand{
+ JobName: val.Name,
+ MCOperation: mcCommand,
+ Command: strings.Join(commands, " "),
+ DepnedsOn: val.DependsOn,
+ Files: files,
+ }
+ // some commands need to have a empty name
+ if jobCommand.JobName == "" {
+ jobCommand.JobName = fmt.Sprintf("command-%d", index)
+ }
+ intervalJob.Command = append(intervalJob.Command, &jobCommand)
+ intervalJob.CommandMap[jobCommand.JobName] = &jobCommand
+ }
+ // check all dependon
+ for _, command := range intervalJob.Command {
+ for _, dep := range command.DepnedsOn {
+ _, found := intervalJob.CommandMap[dep]
+ if !found {
+ return intervalJob, fmt.Errorf("dependent job %s not found", dep)
+ }
+ }
+ }
+ globalIntervalJobStatus.Store(fmt.Sprintf("%s/%s", jobCR.Namespace, jobCR.Name), intervalJob)
+ return intervalJob, nil
+}
+
+var globalIntervalJobStatus = sync.Map{}
diff --git a/pkg/utils/miniojob/minioJob.go b/pkg/utils/miniojob/minioJob.go
new file mode 100644
index 00000000000..de3303da4b7
--- /dev/null
+++ b/pkg/utils/miniojob/minioJob.go
@@ -0,0 +1,172 @@
+// This file is part of MinIO Operator
+// Copyright (c) 2024 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package miniojob
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+)
+
+// Arg - parse the arg result
+type Arg struct {
+ Command string
+ FileName string
+ FileExt string
+ FileContext string
+}
+
+// IsFile - if it is a file
+func (arg Arg) IsFile() bool {
+ return arg.FileName != ""
+}
+
+// FieldsFunc - alias function
+type FieldsFunc func(args map[string]string) (Arg, error)
+
+// Key - key=value|value1,value2,value3
+func Key(key string) FieldsFunc {
+ return KeyForamt(key, "$0")
+}
+
+// FLAGS - --key=""|value|value1,value2,value3
+func FLAGS(ignoreKeys ...string) FieldsFunc {
+ return prefixKeyForamt("-", ignoreKeys...)
+}
+
+// ALIAS - myminio
+func ALIAS() FieldsFunc {
+ return Static("myminio")
+}
+
+// Static - some static value
+func Static(val string) FieldsFunc {
+ return func(args map[string]string) (Arg, error) {
+ return Arg{Command: val}, nil
+ }
+}
+
+// File - fName is the the key, value is content, ext is the file ext
+func File(fName string, ext string) FieldsFunc {
+ return func(args map[string]string) (out Arg, err error) {
+ if args == nil {
+ return out, fmt.Errorf("args is nil")
+ }
+ for key, val := range args {
+ if key == fName {
+ if val == "" {
+ return out, fmt.Errorf("value is empty")
+ }
+ out.FileName = fName
+ out.FileExt = ext
+ out.FileContext = strings.TrimSpace(val)
+ return out, nil
+ }
+ }
+ return out, fmt.Errorf("file %s not found", fName)
+ }
+}
+
+// KeyForamt - match key and get outPut to replace $0 to output the value
+// if format not contain $0, will add $0 to the end
+func KeyForamt(key string, format string) FieldsFunc {
+ return func(args map[string]string) (out Arg, err error) {
+ if args == nil {
+ return out, fmt.Errorf("args is nil")
+ }
+ if !strings.Contains(format, "$0") {
+ format = fmt.Sprintf("%s %s", format, "$0")
+ }
+ val, ok := args[key]
+ if !ok {
+ return out, fmt.Errorf("key %s not found", key)
+ }
+ out.Command = strings.ReplaceAll(format, "$0", strings.ReplaceAll(val, ",", " "))
+ return out, nil
+ }
+}
+
+// OneOf - one of the funcs must be found
+// mc admin policy attach OneOf(--user | --group) = mc admin policy attach --user user or mc admin policy attach --group group
+func OneOf(funcs ...FieldsFunc) FieldsFunc {
+ return func(args map[string]string) (out Arg, err error) {
+ if args == nil {
+ return out, fmt.Errorf("args is nil")
+ }
+ for _, fn := range funcs {
+ if out, err = fn(args); err == nil {
+ return out, nil
+ }
+ }
+ return out, fmt.Errorf("not found")
+ }
+}
+
+// Sanitize - no space for the command
+// mc mb Sanitize(alias / bucketName) = mc mb alias/bucketName
+func Sanitize(funcs ...FieldsFunc) FieldsFunc {
+ return func(args map[string]string) (out Arg, err error) {
+ if args == nil {
+ return out, fmt.Errorf("args is nil")
+ }
+ commands := []string{}
+ for _, func1 := range funcs {
+ if out, err = func1(args); err != nil {
+ return out, err
+ }
+ if out.Command == "" {
+ return out, fmt.Errorf("command is empty")
+ }
+ commands = append(commands, out.Command)
+ }
+ return Arg{Command: strings.Join(commands, "")}, nil
+ }
+}
+
+var prefixKeyForamt = func(pkey string, ignoreKeys ...string) FieldsFunc {
+ return func(args map[string]string) (out Arg, err error) {
+ if args == nil {
+ return out, fmt.Errorf("args is nil")
+ }
+ igrnoreKeyMap := make(map[string]bool)
+ for _, key := range ignoreKeys {
+ if !strings.HasPrefix(key, pkey) {
+ key = fmt.Sprintf("%s%s%s", pkey, pkey, key)
+ }
+ igrnoreKeyMap[key] = true
+ }
+ data := []string{}
+ for key, val := range args {
+ if strings.HasPrefix(key, pkey) && !igrnoreKeyMap[key] {
+ if val == "" {
+ data = append(data, key)
+ } else {
+ for _, singalVal := range strings.Split(val, ",") {
+ if strings.TrimSpace(singalVal) != "" {
+ data = append(data, fmt.Sprintf("%s=%s", key, singalVal))
+ }
+ }
+ }
+ }
+ }
+ // avoid flags change the order
+ sort.Slice(data, func(i, j int) bool {
+ return data[i] > data[j]
+ })
+ return Arg{Command: strings.Join(data, " ")}, nil
+ }
+}
diff --git a/pkg/utils/miniojob/minioJob_test.go b/pkg/utils/miniojob/minioJob_test.go
new file mode 100644
index 00000000000..0f850001e29
--- /dev/null
+++ b/pkg/utils/miniojob/minioJob_test.go
@@ -0,0 +1,168 @@
+// This file is part of MinIO Operator
+// Copyright (c) 2024 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package miniojob
+
+import "testing"
+
+func TestParser(t *testing.T) {
+ args := map[string]string{
+ "--user": "a1,b2,c3,d4",
+ "user": "a,b,c,d",
+ "group": "group1,group2,group3",
+ "password": "somepassword",
+ "--with-locks": "",
+ "--region": "us-west-2",
+ "policy": ` {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:*"
+ ],
+ "Resource": [
+ "arn:aws:s3:::memes",
+ "arn:aws:s3:::memes/*"
+ ]
+ }
+ ]
+ }`,
+ "name": "mybucketName",
+ }
+ testCase := []struct {
+ command FieldsFunc
+ args map[string]string
+ expect Arg
+ expectError bool
+ }{
+ {
+ command: FLAGS("--user"),
+ args: args,
+ expect: Arg{Command: "--with-locks --region=us-west-2"},
+ expectError: false,
+ },
+ {
+ command: FLAGS("user"),
+ args: args,
+ expect: Arg{Command: "--with-locks --region=us-west-2"},
+ expectError: false,
+ },
+ {
+ command: Key("password"),
+ args: args,
+ expect: Arg{Command: "somepassword"},
+ expectError: false,
+ },
+ {
+ command: KeyForamt("user", "--user $0"),
+ args: args,
+ expect: Arg{Command: "--user a b c d"},
+ expectError: false,
+ },
+ {
+ command: KeyForamt("user", "--user"),
+ args: args,
+ expect: Arg{Command: "--user a b c d"},
+ expectError: false,
+ },
+ {
+ command: ALIAS(),
+ args: args,
+ expect: Arg{Command: "myminio"},
+ expectError: false,
+ },
+ {
+ command: Static("test-static"),
+ args: args,
+ expect: Arg{Command: "test-static"},
+ expectError: false,
+ },
+ {
+ command: File("policy", "json"),
+ args: args,
+ expect: Arg{
+ FileName: "policy",
+ FileExt: "json",
+ FileContext: `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:*"
+ ],
+ "Resource": [
+ "arn:aws:s3:::memes",
+ "arn:aws:s3:::memes/*"
+ ]
+ }
+ ]
+ }`,
+ },
+ expectError: false,
+ },
+ {
+ command: OneOf(KeyForamt("user", "--user"), KeyForamt("group", "--group")),
+ args: args,
+ expect: Arg{Command: "--user a b c d"},
+ expectError: false,
+ },
+ {
+ command: OneOf(KeyForamt("miss_user", "--user"), KeyForamt("group", "--group")),
+ args: args,
+ expect: Arg{Command: "--group group1 group2 group3"},
+ expectError: false,
+ },
+ {
+ command: OneOf(KeyForamt("miss_user", "--user"), KeyForamt("miss_group", "--group")),
+ args: args,
+ expect: Arg{Command: "--group group1 group2 group3"},
+ expectError: true,
+ },
+ {
+ command: Sanitize(ALIAS(), Static("/"), Key("name")),
+ args: args,
+ expect: Arg{Command: "myminio/mybucketName"},
+ expectError: false,
+ },
+ }
+ for _, tc := range testCase {
+ cmd, err := tc.command(args)
+ if tc.expectError && err == nil {
+ t.Fatalf("expect error")
+ }
+ if !tc.expectError && err != nil {
+ t.Fatalf("expect not a error")
+ }
+ if !tc.expectError {
+ if tc.expect.Command != "" && cmd.Command != tc.expect.Command {
+ t.Fatalf("expect %s, but got %s", tc.expect, cmd.Command)
+ }
+ if tc.expect.FileName != "" {
+ if tc.expect.FileContext != cmd.FileContext {
+ t.Fatalf("expect %s, but got %s", tc.expect.FileContext, cmd.FileContext)
+ }
+ if tc.expect.FileExt != cmd.FileExt {
+ t.Fatalf("expect %s, but got %s", tc.expect.FileExt, cmd.FileExt)
+ }
+ if tc.expect.FileName != cmd.FileName {
+ t.Fatalf("expect %s, but got %s", tc.expect.FileName, cmd.FileName)
+ }
+ }
+ }
+ }
+}