From 36e48cbe8928d0ae964199e2cb2569139340c8e8 Mon Sep 17 00:00:00 2001
From: jiuker <2818723467@qq.com>
Date: Wed, 27 Mar 2024 01:11:41 +0800
Subject: [PATCH] feat: Create job and Watch job status for minioJob (#2031)
* create job and watch job status for minioJob
watch the minioJob
1.handle the minioJob
2.check ref tenant
3.check sa
4.create the job
5.generate the status to minioJob according to the intervalJob status
watch the job
1.update the intervalJob status
* apply suggestion
* lint
* lint
* lint
* improve the parse
* lint
* lint
* apply suggestion
apply suggestion
* apply suggestion
* doc
* doc
---
pkg/controller/job-controller.go | 458 +++++++++++++++++++++++++++-
pkg/utils/miniojob/minioJob.go | 172 +++++++++++
pkg/utils/miniojob/minioJob_test.go | 168 ++++++++++
3 files changed, 788 insertions(+), 10 deletions(-)
create mode 100644 pkg/utils/miniojob/minioJob.go
create mode 100644 pkg/utils/miniojob/minioJob_test.go
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)
+ }
+ }
+ }
+ }
+}