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) + } + } + } + } +}