From 8b407de9b124df70c055d665d397da2e16fc95fb Mon Sep 17 00:00:00 2001 From: Emeline Gaulard Date: Wed, 21 Mar 2018 16:35:30 +0100 Subject: [PATCH 1/9] add option for private images usage to nerd job run --- cmd/job_run.go | 25 +++++++++++ pkg/kubevisor/visor.go | 6 +++ spec.json | 6 +++ svc/kube_create_secret.go | 94 +++++++++++++++++++++++++++++++++++++++ svc/kube_run_job.go | 7 +++ 5 files changed, 138 insertions(+) create mode 100644 svc/kube_create_secret.go diff --git a/cmd/job_run.go b/cmd/job_run.go index 9fda6fc1a..7430b4b61 100755 --- a/cmd/job_run.go +++ b/cmd/job_run.go @@ -24,6 +24,7 @@ type JobRun struct { VCPU string `long:"vcpu" description:"number of vcpus to use for this job" default:"2"` Inputs []string `long:"input" description:"specify one or more inputs that will be used for the job using the following format: :"` Outputs []string `long:"output" description:"specify one or more output folders that will be stored as datasets after the job is finished using the following format: :"` + Private bool `long:"private" description:"use this flag with a private image, a prompt will ask for your username and password."` //If REGISTRY_USERNAME and/or REGISTRY_PASSWORD are provided, they will be used as values to populate the registry secret *command } @@ -260,6 +261,30 @@ func (cmd *JobRun) Execute(args []string) (err error) { Memory: fmt.Sprintf("%sGi", cmd.Memory), VCPU: cmd.VCPU, } + if cmd.Private { + // extract registry from image name + // list secrets, + // if there is a secret for this registry, use it + // else if REGISTRY_USERNAME and REGISTRY_PASSWORD are provided use them + // else prompt for pwd and username + username, err := cmd.out.Ask("Username: ") + if err != nil { + return err + } + password, err := cmd.out.AskSecret("Password: ") + if err != nil { + return err + } + secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{ + Image: in.Image, + Username: username, + Password: password, + }) + if err != nil { + return renderServiceError(err, "failed to create secret") + } + in.Secret = secret.Name + } for _, vol := range vols { in.Volumes = append(in.Volumes, *vol) diff --git a/pkg/kubevisor/visor.go b/pkg/kubevisor/visor.go index ca84c1680..519d06648 100644 --- a/pkg/kubevisor/visor.go +++ b/pkg/kubevisor/visor.go @@ -49,6 +49,9 @@ var ( //ResourceTypeQuota can be used to retrieve quota information ResourceTypeQuota = ResourceType("resourcequotas") + + //ResourceTypeSecrets can be used to get secret information + ResourceTypeSecrets = ResourceType("secrets") ) //ManagedNames allows for Nerd to transparently manage resources based on names and there prefixes @@ -212,6 +215,9 @@ func (k *Visor) CreateResource(ctx context.Context, t ResourceType, v ManagedNam case ResourceTypeJobs: c = k.api.BatchV1().RESTClient() genfix = "j-" + case ResourceTypeSecrets: + c = k.api.CoreV1().RESTClient() + genfix = "s-" case ResourceTypeDatasets: c = k.crd.NerdalizeV1().RESTClient() genfix = "d-" diff --git a/spec.json b/spec.json index 90d6bcf26..9bbaf50da 100644 --- a/spec.json +++ b/spec.json @@ -431,6 +431,12 @@ "description": "specify one or more output folders that will be stored as datasets after the job is finished using the following format: \u003cDATASET_NAME\u003e:\u003cJOB_DIR\u003e", "default_value": null, "choices": null + }, + { + "long_name": "private", + "description": "use this flag with a private image, a prompt will ask for your username and password.", + "default_value": null, + "choices": null } ] } diff --git a/svc/kube_create_secret.go b/svc/kube_create_secret.go new file mode 100644 index 000000000..4dcab1b3e --- /dev/null +++ b/svc/kube_create_secret.go @@ -0,0 +1,94 @@ +package svc + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + + "github.com/nerdalize/nerd/pkg/kubevisor" + "github.com/pkg/errors" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +//CreateSecretInput is the input to CreateSecret +type CreateSecretInput struct { + Image string `validate:"printascii"` + Username string + Password string +} + +//CreateSecretOutput is the output to CreateSecret +type CreateSecretOutput struct { + Name string +} + +//CreateSecret will create a secret on kubernetes +func (k *Kube) CreateSecret(ctx context.Context, in *CreateSecretInput) (out *CreateSecretOutput, err error) { + if err = k.checkInput(ctx, in); err != nil { + return nil, err + } + + image, project, registry := extractRegistry(in.Image) + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"image": image, "project": project, "registry": registry}, + }, + Type: v1.SecretTypeDockerConfigJson, + Data: map[string][]byte{}, + } + + var dockerCfg []byte + auths := map[string]interface{}{} + cfg := map[string]interface{}{ + "auths": auths, + "HttpHeaders": map[string]interface{}{ + "User-Agent": "Docker-Client/1.11.2 (linux)", + }, + } + authStr := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", in.Username, in.Password))) + auths[fmt.Sprintf("https://%s/v1/", registry)] = map[string]string{ + "auth": authStr, + } + auths[fmt.Sprintf("%s", registry)] = map[string]string{ + "auth": authStr, + } + if dockerCfg, err = json.Marshal(cfg); err != nil { + return nil, errors.Wrap(err, "failed to serialize docker secret cfg") + } + secret.Data[v1.DockerConfigJsonKey] = dockerCfg + + err = k.visor.CreateResource(ctx, kubevisor.ResourceTypeSecrets, secret, "") + if err != nil { + return nil, err + } + + return &CreateSecretOutput{ + Name: secret.Name, + }, nil +} + +func extractRegistry(image string) (string, string, string) { + // Supported registries: + // - project/image -> index.docker.io + // - ACCOUNT.dkr.ecr.REGION.amazonaws.com/image -> aws + // - azurecr.io/image -> azure + // - quay.io/project/image -> quay.io + // - gcr.io/project/image -> gcr + // gitlab?? other providers? + + parts := strings.Split(image, "/") + switch len(parts) { + case 2: + if !strings.Contains(parts[0], ".") { + return parts[1], parts[0], "index.docker.io" + } + return parts[1], "", parts[0] + case 3: + return parts[2], parts[1], parts[0] + } + return "", "", "" +} diff --git a/svc/kube_run_job.go b/svc/kube_run_job.go index 82e67555f..95f89c862 100755 --- a/svc/kube_run_job.go +++ b/svc/kube_run_job.go @@ -28,6 +28,7 @@ type RunJobInput struct { Volumes []JobVolume Memory string VCPU string + Secret string } //JobVolumeType determines if its content will be uploaded or downloaded @@ -130,6 +131,12 @@ func (k *Kube) RunJob(ctx context.Context, in *RunJobInput) (out *RunJobOutput, }) } + if in.Secret != "" { + job.Spec.Template.Spec.ImagePullSecrets = append(job.Spec.Template.Spec.ImagePullSecrets, v1.LocalObjectReference{ + Name: kubevisor.DefaultPrefix + in.Secret, + }) + } + err = k.visor.CreateResource(ctx, kubevisor.ResourceTypeJobs, job, in.Name) if err != nil { return nil, err From 9ef00a85c3daae7c8fd81214e01864764cd0295a Mon Sep 17 00:00:00 2001 From: Emeline Gaulard Date: Thu, 22 Mar 2018 10:57:32 +0100 Subject: [PATCH 2/9] add tests for secret creation --- cmd/job_run.go | 2 +- svc/kube_create_secret_test.go | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 svc/kube_create_secret_test.go diff --git a/cmd/job_run.go b/cmd/job_run.go index 7430b4b61..cfa97a645 100755 --- a/cmd/job_run.go +++ b/cmd/job_run.go @@ -265,7 +265,7 @@ func (cmd *JobRun) Execute(args []string) (err error) { // extract registry from image name // list secrets, // if there is a secret for this registry, use it - // else if REGISTRY_USERNAME and REGISTRY_PASSWORD are provided use them + // else if DOCKER_USERNAME and DOCKER_PASSWORD are provided use them // else prompt for pwd and username username, err := cmd.out.Ask("Username: ") if err != nil { diff --git a/svc/kube_create_secret_test.go b/svc/kube_create_secret_test.go new file mode 100644 index 000000000..6d1c1b381 --- /dev/null +++ b/svc/kube_create_secret_test.go @@ -0,0 +1,61 @@ +package svc_test + +import ( + "context" + "fmt" + "reflect" + "runtime" + "strings" + "testing" + "time" + + "github.com/nerdalize/nerd/svc" +) + +func TestCreateSecret(t *testing.T) { + for _, c := range []struct { + Name string + Timeout time.Duration + Input *svc.CreateSecretInput + IsOutput func(tb testing.TB, out *svc.CreateSecretOutput) + IsErr func(error) bool + }{ + { + Name: "when a zero value input is provided it should return a validation error", + Timeout: time.Second * 5, + Input: nil, + IsErr: svc.IsValidationErr, + IsOutput: func(t testing.TB, out *svc.CreateSecretOutput) { + assert(t, out == nil, "output should be nil") + }, + }, + { + Name: "when a valid input is provided it should return a secret with a unique name", + Timeout: time.Second * 5, + Input: &svc.CreateSecretInput{Image: "quay.io/nerdalize/smoketest", Username: "test", Password: "test"}, + IsErr: nil, + IsOutput: func(t testing.TB, out *svc.CreateSecretOutput) { + assert(t, strings.Contains(out.Name, "s-"), "secret name should be generated and prefixed") + }, + }, + } { + t.Run(c.Name, func(t *testing.T) { + di, clean := testDI(t) + defer clean() + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, c.Timeout) + defer cancel() + + kube := svc.NewKube(di) + out, err := kube.CreateSecret(ctx, c.Input) + if c.IsErr != nil { + assert(t, c.IsErr(err), fmt.Sprintf("unexpected '%#v' to match: %#v", err, runtime.FuncForPC(reflect.ValueOf(c.IsErr).Pointer()).Name())) + } + + if c.IsOutput != nil { + c.IsOutput(t, out) + } + }) + } +} From e25615c355f450e5b893060ed029c8b731fd2692 Mon Sep 17 00:00:00 2001 From: Emeline Gaulard Date: Thu, 22 Mar 2018 12:00:58 +0100 Subject: [PATCH 3/9] Use environment variable to populate secret --- cmd/job_run.go | 29 ++++++++++++++++++++++------- spec.json | 2 +- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/cmd/job_run.go b/cmd/job_run.go index cfa97a645..98b8228e5 100755 --- a/cmd/job_run.go +++ b/cmd/job_run.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "os" "path/filepath" "strconv" "strings" @@ -24,8 +25,7 @@ type JobRun struct { VCPU string `long:"vcpu" description:"number of vcpus to use for this job" default:"2"` Inputs []string `long:"input" description:"specify one or more inputs that will be used for the job using the following format: :"` Outputs []string `long:"output" description:"specify one or more output folders that will be stored as datasets after the job is finished using the following format: :"` - Private bool `long:"private" description:"use this flag with a private image, a prompt will ask for your username and password."` //If REGISTRY_USERNAME and/or REGISTRY_PASSWORD are provided, they will be used as values to populate the registry secret - + Private bool `long:"private" description:"use this flag with a private image, a prompt will ask for your username and password. If DOCKER_USERNAME and/or DOCKER_PASSWORD are provided, they will be used as values to populate the registry secret."` *command } @@ -267,14 +267,11 @@ func (cmd *JobRun) Execute(args []string) (err error) { // if there is a secret for this registry, use it // else if DOCKER_USERNAME and DOCKER_PASSWORD are provided use them // else prompt for pwd and username - username, err := cmd.out.Ask("Username: ") - if err != nil { - return err - } - password, err := cmd.out.AskSecret("Password: ") + username, password, err := cmd.getCredentials() if err != nil { return err } + fmt.Println(username, password) secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{ Image: in.Image, Username: username, @@ -343,6 +340,24 @@ func (cmd *JobRun) rollbackDatasets(ctx context.Context, mgr transfer.Manager, i return err } +func (cmd *JobRun) getCredentials() (username, password string, err error) { + username = os.Getenv("DOCKER_USERNAME") + if username == "" { + username, err = cmd.out.Ask("Username: ") + if err != nil { + return username, password, err + } + } + password = os.Getenv("DOCKER_PASSWORD") + if password == "" { + password, err = cmd.out.AskSecret("Password: ") + if err != nil { + return username, password, err + } + } + return username, password, err +} + func updateDatasets(ctx context.Context, kube *svc.Kube, inputs, outputs []dsHandle, name string) error { //add job to each dataset's InputFor for _, input := range inputs { diff --git a/spec.json b/spec.json index 9bbaf50da..746636d49 100644 --- a/spec.json +++ b/spec.json @@ -434,7 +434,7 @@ }, { "long_name": "private", - "description": "use this flag with a private image, a prompt will ask for your username and password.", + "description": "use this flag with a private image, a prompt will ask for your username and password. If DOCKER_USERNAME and/or DOCKER_PASSWORD are provided, they will be used as values to populate the registry secret.", "default_value": null, "choices": null } From 75e862d1e9cf1cd48f831bd97cca66e1b2a21e63 Mon Sep 17 00:00:00 2001 From: Emeline Gaulard Date: Thu, 22 Mar 2018 12:04:48 +0100 Subject: [PATCH 4/9] remove useless print statement --- cmd/job_run.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/job_run.go b/cmd/job_run.go index 98b8228e5..4a50827eb 100755 --- a/cmd/job_run.go +++ b/cmd/job_run.go @@ -265,13 +265,10 @@ func (cmd *JobRun) Execute(args []string) (err error) { // extract registry from image name // list secrets, // if there is a secret for this registry, use it - // else if DOCKER_USERNAME and DOCKER_PASSWORD are provided use them - // else prompt for pwd and username username, password, err := cmd.getCredentials() if err != nil { return err } - fmt.Println(username, password) secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{ Image: in.Image, Username: username, From f8af4006eb8e19f220ec18c89ccabe5a9ea46948 Mon Sep 17 00:00:00 2001 From: Emeline Gaulard Date: Thu, 22 Mar 2018 14:46:36 +0100 Subject: [PATCH 5/9] Add functions + tests to manage secrets --- pkg/kubevisor/visor.go | 11 +++++------ svc/kube_delete_dataset_test.go | 11 +++++++++++ svc/kube_get_dataset.go | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pkg/kubevisor/visor.go b/pkg/kubevisor/visor.go index 519d06648..8c8750aef 100644 --- a/pkg/kubevisor/visor.go +++ b/pkg/kubevisor/visor.go @@ -115,6 +115,8 @@ func (k *Visor) GetResource(ctx context.Context, t ResourceType, v ManagedNames, switch t { case ResourceTypeJobs: c = k.api.BatchV1().RESTClient() + case ResourceTypeSecrets: + c = k.api.CoreV1().RESTClient() case ResourceTypeDatasets: c = k.crd.NerdalizeV1().RESTClient() default: @@ -149,7 +151,8 @@ func (k *Visor) DeleteResource(ctx context.Context, t ResourceType, name string) c = k.api.BatchV1().RESTClient() case ResourceTypeDatasets: c = k.crd.NerdalizeV1().RESTClient() - + case ResourceTypeSecrets: + c = k.api.CoreV1().RESTClient() default: return errors.Errorf("unknown Kubernetes resource type provided for deletion: '%s'", t) } @@ -309,11 +312,7 @@ func (k *Visor) ListResources(ctx context.Context, t ResourceType, v ListTranfor switch t { case ResourceTypeJobs: c = k.api.BatchV1().RESTClient() - case ResourceTypePods: - c = k.api.CoreV1().RESTClient() - case ResourceTypeEvents: - c = k.api.CoreV1().RESTClient() - case ResourceTypeQuota: + case ResourceTypePods, ResourceTypeEvents, ResourceTypeQuota, ResourceTypeSecrets: c = k.api.CoreV1().RESTClient() case ResourceTypeDatasets: c = k.crd.NerdalizeV1().RESTClient() diff --git a/svc/kube_delete_dataset_test.go b/svc/kube_delete_dataset_test.go index 0a2aa68f3..8dad65a16 100644 --- a/svc/kube_delete_dataset_test.go +++ b/svc/kube_delete_dataset_test.go @@ -8,7 +8,10 @@ import ( "testing" "time" + "github.com/nerdalize/nerd/pkg/transfer/archiver" + "github.com/nerdalize/nerd/pkg/kubevisor" + "github.com/nerdalize/nerd/pkg/transfer/store" "github.com/nerdalize/nerd/svc" ) @@ -37,6 +40,14 @@ func TestDeleteDataset(t *testing.T) { Output: &svc.DeleteDatasetOutput{}, IsErr: kubevisor.IsNotExistsErr, }, + { + Name: "when a valid dataset is deleted it should return no error", + Timeout: time.Second * 5, + Datasets: []*svc.CreateDatasetInput{{Name: "test", StoreOptions: transferstore.StoreOptions{}, ArchiverOptions: transferarchiver.ArchiverOptions{}}}, + Input: &svc.DeleteDatasetInput{Name: "test"}, + Output: &svc.DeleteDatasetOutput{}, + IsErr: nil, + }, } { t.Run(c.Name, func(t *testing.T) { di, clean := testDI(t) diff --git a/svc/kube_get_dataset.go b/svc/kube_get_dataset.go index 82dce94d9..f6f6d61bc 100644 --- a/svc/kube_get_dataset.go +++ b/svc/kube_get_dataset.go @@ -27,7 +27,7 @@ type GetDatasetOutput struct { ArchiverOptions transferarchiver.ArchiverOptions } -//GetDataset will create a dataset on kubernetes +//GetDataset will retrieve a dataset from kubernetes func (k *Kube) GetDataset(ctx context.Context, in *GetDatasetInput) (out *GetDatasetOutput, err error) { if err = k.checkInput(ctx, in); err != nil { return nil, err From 445797d821360253f0a89da20c7b758edf8ccaf6 Mon Sep 17 00:00:00 2001 From: Emeline Gaulard Date: Thu, 22 Mar 2018 14:50:59 +0100 Subject: [PATCH 6/9] One secret per image, and these secrets are updated each time a private image is reused --- cmd/job_run.go | 34 ++++++++---- pkg/kubevisor/visor.go | 2 +- svc/kube_create_secret.go | 31 ++++++----- svc/kube_create_secret_test.go | 1 + svc/kube_delete_secret.go | 29 ++++++++++ svc/kube_delete_secret_test.go | 92 +++++++++++++++++++++++++++++++ svc/kube_get_secret.go | 46 ++++++++++++++++ svc/kube_get_secret_test.go | 98 ++++++++++++++++++++++++++++++++++ svc/kube_list_secret.go | 86 +++++++++++++++++++++++++++++ svc/kube_list_secrets_test.go | 87 ++++++++++++++++++++++++++++++ svc/kube_update_dataset.go | 2 - svc/kube_update_secret.go | 42 +++++++++++++++ svc/kube_update_secret_test.go | 33 ++++++++++++ 13 files changed, 558 insertions(+), 25 deletions(-) create mode 100644 svc/kube_delete_secret.go create mode 100644 svc/kube_delete_secret_test.go create mode 100644 svc/kube_get_secret.go create mode 100644 svc/kube_get_secret_test.go create mode 100644 svc/kube_list_secret.go create mode 100644 svc/kube_list_secrets_test.go create mode 100644 svc/kube_update_secret.go create mode 100644 svc/kube_update_secret_test.go diff --git a/cmd/job_run.go b/cmd/job_run.go index 4a50827eb..d409101da 100755 --- a/cmd/job_run.go +++ b/cmd/job_run.go @@ -262,22 +262,36 @@ func (cmd *JobRun) Execute(args []string) (err error) { VCPU: cmd.VCPU, } if cmd.Private { - // extract registry from image name - // list secrets, - // if there is a secret for this registry, use it username, password, err := cmd.getCredentials() if err != nil { return err } - secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{ - Image: in.Image, - Username: username, - Password: password, - }) + + secrets, err := kube.ListSecrets(ctx, &svc.ListSecretsInput{}) if err != nil { - return renderServiceError(err, "failed to create secret") + return renderServiceError(err, "failed to list secrets") + } + for _, secret := range secrets.Items { + if secret.Details.Image == in.Image { + _, err = kube.UpdateSecret(ctx, &svc.UpdateSecretInput{Name: secret.Name, Username: username, Password: password}) + if err != nil { + return renderServiceError(err, "failed to update secret") + } + in.Secret = secret.Name + break + } + } + if in.Secret == "" { + secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{ + Image: in.Image, + Username: username, + Password: password, + }) + if err != nil { + return renderServiceError(err, "failed to create secret") + } + in.Secret = secret.Name } - in.Secret = secret.Name } for _, vol := range vols { diff --git a/pkg/kubevisor/visor.go b/pkg/kubevisor/visor.go index 8c8750aef..fabe42989 100644 --- a/pkg/kubevisor/visor.go +++ b/pkg/kubevisor/visor.go @@ -271,7 +271,7 @@ func (k *Visor) UpdateResource(ctx context.Context, t ResourceType, v ManagedNam switch t { case ResourceTypeJobs: c = k.api.BatchV1().RESTClient() - case ResourceTypePods: + case ResourceTypePods, ResourceTypeSecrets: c = k.api.CoreV1().RESTClient() case ResourceTypeDatasets: c = k.crd.NerdalizeV1().RESTClient() diff --git a/svc/kube_create_secret.go b/svc/kube_create_secret.go index 4dcab1b3e..19c80d9b2 100644 --- a/svc/kube_create_secret.go +++ b/svc/kube_create_secret.go @@ -41,6 +41,22 @@ func (k *Kube) CreateSecret(ctx context.Context, in *CreateSecretInput) (out *Cr Data: map[string][]byte{}, } + secret.Data[v1.DockerConfigJsonKey], err = transformCredentials(in.Username, in.Password, registry) + if err != nil { + return nil, err + } + + err = k.visor.CreateResource(ctx, kubevisor.ResourceTypeSecrets, secret, "") + if err != nil { + return nil, err + } + + return &CreateSecretOutput{ + Name: secret.Name, + }, nil +} + +func transformCredentials(username, password, registry string) (dockereCfg []byte, err error) { var dockerCfg []byte auths := map[string]interface{}{} cfg := map[string]interface{}{ @@ -49,7 +65,7 @@ func (k *Kube) CreateSecret(ctx context.Context, in *CreateSecretInput) (out *Cr "User-Agent": "Docker-Client/1.11.2 (linux)", }, } - authStr := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", in.Username, in.Password))) + authStr := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) auths[fmt.Sprintf("https://%s/v1/", registry)] = map[string]string{ "auth": authStr, } @@ -57,18 +73,9 @@ func (k *Kube) CreateSecret(ctx context.Context, in *CreateSecretInput) (out *Cr "auth": authStr, } if dockerCfg, err = json.Marshal(cfg); err != nil { - return nil, errors.Wrap(err, "failed to serialize docker secret cfg") + return dockerCfg, errors.Wrap(err, "failed to serialize docker secret cfg") } - secret.Data[v1.DockerConfigJsonKey] = dockerCfg - - err = k.visor.CreateResource(ctx, kubevisor.ResourceTypeSecrets, secret, "") - if err != nil { - return nil, err - } - - return &CreateSecretOutput{ - Name: secret.Name, - }, nil + return dockerCfg, nil } func extractRegistry(image string) (string, string, string) { diff --git a/svc/kube_create_secret_test.go b/svc/kube_create_secret_test.go index 6d1c1b381..5028f2c83 100644 --- a/svc/kube_create_secret_test.go +++ b/svc/kube_create_secret_test.go @@ -35,6 +35,7 @@ func TestCreateSecret(t *testing.T) { Input: &svc.CreateSecretInput{Image: "quay.io/nerdalize/smoketest", Username: "test", Password: "test"}, IsErr: nil, IsOutput: func(t testing.TB, out *svc.CreateSecretOutput) { + assert(t, out != nil, "output should not be nil") assert(t, strings.Contains(out.Name, "s-"), "secret name should be generated and prefixed") }, }, diff --git a/svc/kube_delete_secret.go b/svc/kube_delete_secret.go new file mode 100644 index 000000000..523dc86c6 --- /dev/null +++ b/svc/kube_delete_secret.go @@ -0,0 +1,29 @@ +package svc + +import ( + "context" + + "github.com/nerdalize/nerd/pkg/kubevisor" +) + +//DeleteSecretInput is the input to DeleteSecret +type DeleteSecretInput struct { + Name string `validate:"min=1,printascii"` +} + +//DeleteSecretOutput is the output to DeleteSecret +type DeleteSecretOutput struct{} + +//DeleteSecret will create a dataset on kubernetes +func (k *Kube) DeleteSecret(ctx context.Context, in *DeleteSecretInput) (out *DeleteSecretOutput, err error) { + if err = k.checkInput(ctx, in); err != nil { + return nil, err + } + + err = k.visor.DeleteResource(ctx, kubevisor.ResourceTypeSecrets, in.Name) + if err != nil { + return nil, err + } + + return &DeleteSecretOutput{}, nil +} diff --git a/svc/kube_delete_secret_test.go b/svc/kube_delete_secret_test.go new file mode 100644 index 000000000..7a9297bab --- /dev/null +++ b/svc/kube_delete_secret_test.go @@ -0,0 +1,92 @@ +package svc_test + +import ( + "context" + "fmt" + "reflect" + "runtime" + "testing" + "time" + + "github.com/nerdalize/nerd/pkg/kubevisor" + "github.com/nerdalize/nerd/svc" +) + +func TestDeleteSecret(t *testing.T) { + for _, c := range []struct { + Name string + Timeout time.Duration + Secrets []*svc.CreateSecretInput + Input *svc.DeleteSecretInput + Output *svc.DeleteSecretOutput + Listing *svc.ListSecretsOutput + IsOutput func(tb testing.TB, out *svc.DeleteSecretOutput, l *svc.ListSecretsOutput) + IsErr func(error) bool + }{ + { + Name: "when no name is provided it should provide a validation error", + Timeout: time.Second * 5, + Input: &svc.DeleteSecretInput{}, + Output: &svc.DeleteSecretOutput{}, + IsErr: svc.IsValidationErr, + }, + { + Name: "when a non-existing secret is deleted it should return NotExists error", + Timeout: time.Second * 5, + Input: &svc.DeleteSecretInput{Name: "foo"}, + Output: &svc.DeleteSecretOutput{}, + IsErr: kubevisor.IsNotExistsErr, + }, + } { + t.Run(c.Name, func(t *testing.T) { + di, clean := testDI(t) + defer clean() + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, c.Timeout) + defer cancel() + + kube := svc.NewKube(di) + for _, secret := range c.Secrets { + _, err := kube.CreateSecret(ctx, secret) + ok(t, err) + } + + out, err := kube.DeleteSecret(ctx, c.Input) + if c.IsErr != nil { + assert(t, c.IsErr(err), fmt.Sprintf("unexpected '%#v' to match: %#v", err, runtime.FuncForPC(reflect.ValueOf(c.IsErr).Pointer()).Name())) + } + + list, err := kube.ListSecrets(ctx, &svc.ListSecretsInput{}) + ok(t, err) + + if c.IsOutput != nil { + c.IsOutput(t, out, list) + } + }) + } +} + +func TestDeleteSpecificSecret(t *testing.T) { + image := "quay.io/nerdalize/smoketest" + timeout := time.Minute + + if testing.Short() { + t.Skipf("skipping long test with contex timeout: %s", timeout) + } + + di, clean := testDI(t) + defer clean() + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + kube := svc.NewKube(di) + secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{Image: image, Username: "test", Password: "test"}) + ok(t, err) + + out, err := kube.DeleteSecret(ctx, &svc.DeleteSecretInput{Name: secret.Name}) + ok(t, err) + assert(t, out != nil, "expected to find a DeleteSecretOutput") +} diff --git a/svc/kube_get_secret.go b/svc/kube_get_secret.go new file mode 100644 index 000000000..7b4be17b0 --- /dev/null +++ b/svc/kube_get_secret.go @@ -0,0 +1,46 @@ +package svc + +import ( + "context" + "path" + "time" + + "github.com/nerdalize/nerd/pkg/kubevisor" + "k8s.io/api/core/v1" +) + +//GetSecretInput is the input to GetSecret +type GetSecretInput struct { + Name string `validate:"printascii"` +} + +//GetSecretOutput is the output to GetSecret +type GetSecretOutput struct { + Name string + Size int + Image string + CreatedAt time.Time + Type string +} + +//GetSecret will retrieve the secret matching the provided name from kubernetes +func (k *Kube) GetSecret(ctx context.Context, in *GetSecretInput) (out *GetSecretOutput, err error) { + if err = k.checkInput(ctx, in); err != nil { + return nil, err + } + + secret := &v1.Secret{} + err = k.visor.GetResource(ctx, kubevisor.ResourceTypeSecrets, secret, in.Name) + if err != nil { + return nil, err + } + + return &GetSecretOutput{ + Name: secret.Name, + Type: string(secret.Type), + Size: secret.Size(), + CreatedAt: secret.CreationTimestamp.Local(), + Image: path.Join(secret.Labels["registry"], secret.Labels["project"], secret.Labels["image"]), + }, nil + +} diff --git a/svc/kube_get_secret_test.go b/svc/kube_get_secret_test.go new file mode 100644 index 000000000..f141faee3 --- /dev/null +++ b/svc/kube_get_secret_test.go @@ -0,0 +1,98 @@ +package svc_test + +import ( + "context" + "fmt" + "reflect" + "runtime" + "testing" + "time" + + "github.com/nerdalize/nerd/pkg/kubevisor" + "github.com/nerdalize/nerd/svc" +) + +func TestGetSecret(t *testing.T) { + for _, c := range []struct { + Name string + Timeout time.Duration + Secrets []*svc.CreateSecretInput + Input *svc.GetSecretInput + IsOutput func(tb testing.TB, out *svc.GetSecretOutput) bool + IsErr func(error) bool + }{ + { + Name: "when a zero value input is provided it should return a validation error", + Timeout: time.Second * 5, + Secrets: nil, + Input: nil, + IsErr: svc.IsValidationErr, + IsOutput: func(t testing.TB, out *svc.GetSecretOutput) bool { + return true + }, + }, + { + Name: "when secret doesnt exist it should return an error", + Timeout: time.Second * 5, + Input: &svc.GetSecretInput{Name: "my-secret"}, + IsErr: kubevisor.IsNotExistsErr, + IsOutput: func(t testing.TB, out *svc.GetSecretOutput) bool { + return true + }, + }, + } { + t.Run(c.Name, func(t *testing.T) { + if c.Timeout > time.Second*5 && testing.Short() { + t.Skipf("skipping long test with contex timeout: %s", c.Timeout) + } + + di, clean := testDI(t) + defer clean() + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, c.Timeout) + defer cancel() + + kube := svc.NewKube(di) + for _, secrets := range c.Secrets { + _, err := kube.CreateSecret(ctx, secrets) + ok(t, err) + } + + out, err := kube.GetSecret(ctx, c.Input) + if c.IsErr != nil { //if c.IsErr is nil we dont care about errors + assert(t, c.IsErr(err), fmt.Sprintf("unexpected '%#v' to match: %#v", err, runtime.FuncForPC(reflect.ValueOf(c.IsErr).Pointer()).Name())) + } + + if c.IsOutput != nil { + c.IsOutput(t, out) + } + }) + } +} + +func TestGetSpecificSecret(t *testing.T) { + image := "quay.io/nerdalize/smoketest" + timeout := time.Minute + + if testing.Short() { + t.Skipf("skipping long test with contex timeout: %s", timeout) + } + + di, clean := testDI(t) + defer clean() + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + kube := svc.NewKube(di) + secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{Image: image, Username: "test", Password: "test"}) + ok(t, err) + + out, err := kube.GetSecret(ctx, &svc.GetSecretInput{Name: secret.Name}) + ok(t, err) + assert(t, out != nil, "expected to find a secret") + assert(t, out.Image == image, "expected to find the base image in the secret") + +} diff --git a/svc/kube_list_secret.go b/svc/kube_list_secret.go new file mode 100644 index 000000000..26aeafab8 --- /dev/null +++ b/svc/kube_list_secret.go @@ -0,0 +1,86 @@ +package svc + +import ( + "context" + "path" + "time" + + "github.com/nerdalize/nerd/pkg/kubevisor" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" +) + +//SecretDetails tells us more about the secret by looking at underlying resources +type SecretDetails struct { + CreatedAt time.Time + Size int + Type string + Image string +} + +//ListSecretItem is a secret listing item +type ListSecretItem struct { + Name string + Details SecretDetails +} + +//ListSecretsInput is the input to ListSecrets +type ListSecretsInput struct { + Labels []string +} + +//ListSecretsOutput is the output to ListSecrets +type ListSecretsOutput struct { + Items []*ListSecretItem +} + +//ListSecrets will create a secret on kubernetes +func (k *Kube) ListSecrets(ctx context.Context, in *ListSecretsInput) (out *ListSecretsOutput, err error) { + if err = k.checkInput(ctx, in); err != nil { + return nil, err + } + + //Step 0: Get all the secrets under nerd-app=cli + secrets := &secrets{} + err = k.visor.ListResources(ctx, kubevisor.ResourceTypeSecrets, secrets, in.Labels, nil) + if err != nil { + return nil, err + } + + //Step 1: Analyse secret structure and formulate our output items + out = &ListSecretsOutput{} + mapping := map[types.UID]*ListSecretItem{} + for _, secret := range secrets.Items { + if secret.Labels["registry"] == "index.docker.io" { + secret.Labels["registry"] = "" + } + item := &ListSecretItem{ + Name: secret.GetName(), + Details: SecretDetails{ + Type: string(secret.Type), + Size: secret.Size(), + CreatedAt: secret.CreationTimestamp.Local(), + Image: path.Join(secret.Labels["registry"], secret.Labels["project"], secret.Labels["image"]), + }, + } + + mapping[secret.UID] = item + out.Items = append(out.Items, item) + } + + return out, nil +} + +//secrets implements the list transformer interface to allow the kubevisor to manage names for us +type secrets struct{ *v1.SecretList } + +func (secrets *secrets) Transform(fn func(in kubevisor.ManagedNames) (out kubevisor.ManagedNames)) { + for i, d1 := range secrets.SecretList.Items { + secrets.Items[i] = *(fn(&d1).(*v1.Secret)) + } +} + +func (secrets *secrets) Len() int { + return len(secrets.SecretList.Items) +} diff --git a/svc/kube_list_secrets_test.go b/svc/kube_list_secrets_test.go new file mode 100644 index 000000000..413ee3b82 --- /dev/null +++ b/svc/kube_list_secrets_test.go @@ -0,0 +1,87 @@ +package svc_test + +import ( + "context" + "fmt" + "reflect" + "runtime" + "strings" + "testing" + "time" + + "github.com/nerdalize/nerd/svc" +) + +func TestListSecrets(t *testing.T) { + for _, c := range []struct { + Name string + Timeout time.Duration + Secrets []*svc.CreateSecretInput + Input *svc.ListSecretsInput + IsOutput func(tb testing.TB, out *svc.ListSecretsOutput) bool + IsErr func(error) bool + }{ + { + Name: "when a zero value input is provided it should return a validation error", + Timeout: time.Second * 5, + Secrets: nil, + Input: nil, + IsErr: svc.IsValidationErr, + IsOutput: func(t testing.TB, out *svc.ListSecretsOutput) bool { + return true + }, + }, + { + Name: "when no secrets have been created the output should be empty", + Timeout: time.Second * 5, + Input: &svc.ListSecretsInput{}, + IsErr: isNilErr, + IsOutput: func(t testing.TB, out *svc.ListSecretsOutput) bool { + assert(t, len(out.Items) == 0, "expected zero secrets to be listed") + return true + }, + }, + { + Name: "when one correct secret was created it should be listed", + Timeout: time.Minute, + Secrets: []*svc.CreateSecretInput{{Image: "quay.io/nerdalize/smoketest", Username: "test", Password: "test"}}, + Input: &svc.ListSecretsInput{}, + IsErr: isNilErr, + IsOutput: func(t testing.TB, out *svc.ListSecretsOutput) bool { + assert(t, len(out.Items) == 1, "expected one secret to be listed") + assert(t, !out.Items[0].Details.CreatedAt.IsZero(), "created at time should not be zero") + + assert(t, strings.HasPrefix(out.Items[0].Name, "s-"), "expected secret name to be prefixed has expected") + return true + }, + }, + } { + t.Run(c.Name, func(t *testing.T) { + if c.Timeout > time.Second*5 && testing.Short() { + t.Skipf("skipping long test with contex timeout: %s", c.Timeout) + } + + di, clean := testDI(t) + defer clean() + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, c.Timeout) + defer cancel() + + kube := svc.NewKube(di) + for _, secret := range c.Secrets { + _, err := kube.CreateSecret(ctx, secret) + ok(t, err) + } + + out, err := kube.ListSecrets(ctx, c.Input) + if c.IsErr != nil { + assert(t, c.IsErr(err), fmt.Sprintf("unexpected '%#v' to match: %#v", err, runtime.FuncForPC(reflect.ValueOf(c.IsErr).Pointer()).Name())) + } + + if c.IsOutput != nil { + c.IsOutput(t, out) + } + }) + } +} diff --git a/svc/kube_update_dataset.go b/svc/kube_update_dataset.go index f79bdce6c..2d84e2c86 100755 --- a/svc/kube_update_dataset.go +++ b/svc/kube_update_dataset.go @@ -2,7 +2,6 @@ package svc import ( "context" - "log" "github.com/nerdalize/nerd/pkg/kubevisor" @@ -47,7 +46,6 @@ func (k *Kube) UpdateDataset(ctx context.Context, in *UpdateDatasetInput) (out * err = k.visor.UpdateResource(ctx, kubevisor.ResourceTypeDatasets, dataset, in.Name) if err != nil { - log.Println(err) return nil, err } return &UpdateDatasetOutput{ diff --git a/svc/kube_update_secret.go b/svc/kube_update_secret.go new file mode 100644 index 000000000..92813c430 --- /dev/null +++ b/svc/kube_update_secret.go @@ -0,0 +1,42 @@ +package svc + +import ( + "context" + + "github.com/nerdalize/nerd/pkg/kubevisor" + "k8s.io/api/core/v1" +) + +// UpdateSecretInput is the input for UpdateSecret +type UpdateSecretInput struct { + Name string `validate:"printascii"` + Username string + Password string +} + +// UpdateSecretOutput is the output for UpdateSecret +type UpdateSecretOutput struct { + Name string +} + +// UpdateSecret will update a secret resource. +// Fields that can be updated: name, input, output and size. Input and output are the jobs the secret is used for or coming from. +func (k *Kube) UpdateSecret(ctx context.Context, in *UpdateSecretInput) (out *UpdateSecretOutput, err error) { + secret := &v1.Secret{} + err = k.visor.GetResource(ctx, kubevisor.ResourceTypeSecrets, secret, in.Name) + if err != nil { + return nil, err + } + + secret.Data[v1.DockerConfigJsonKey], err = transformCredentials(in.Username, in.Password, secret.Labels["registry"]) + if err != nil { + return nil, err + } + err = k.visor.UpdateResource(ctx, kubevisor.ResourceTypeSecrets, secret, in.Name) + if err != nil { + return nil, err + } + return &UpdateSecretOutput{ + Name: secret.Name, + }, nil +} diff --git a/svc/kube_update_secret_test.go b/svc/kube_update_secret_test.go new file mode 100644 index 000000000..5c8b787e1 --- /dev/null +++ b/svc/kube_update_secret_test.go @@ -0,0 +1,33 @@ +package svc_test + +import ( + "context" + "testing" + "time" + + "github.com/nerdalize/nerd/svc" +) + +func TestUpdateSecret(t *testing.T) { + di, clean := testDI(t) + defer clean() + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Second*5) + defer cancel() + + kube := svc.NewKube(di) + out, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{ + Image: "quay.io/nerdalize/smoketest", + Username: "test", + Password: "test", + }) + ok(t, err) + + _, err = kube.UpdateSecret(ctx, &svc.UpdateSecretInput{ + Name: out.Name, + Username: "newtest", + Password: "newtest", + }) + ok(t, err) +} From 1d8111f3681f8a9fe31c2ed5a86bbbe306282880 Mon Sep 17 00:00:00 2001 From: Emeline Gaulard Date: Thu, 22 Mar 2018 16:43:47 +0100 Subject: [PATCH 7/9] Prompt again for credentials only if --clean-creds is used --- cmd/job_run.go | 36 +++++++++++++++++++++--------------- spec.json | 6 ++++++ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/cmd/job_run.go b/cmd/job_run.go index d409101da..535beb6c4 100755 --- a/cmd/job_run.go +++ b/cmd/job_run.go @@ -19,13 +19,14 @@ import ( //JobRun command type JobRun struct { - Name string `long:"name" short:"n" description:"assign a name to the job"` - Env []string `long:"env" short:"e" description:"environment variables to use"` - Memory string `long:"memory" short:"m" description:"memory to use for this job, expressed in gigabytes" default:"3"` - VCPU string `long:"vcpu" description:"number of vcpus to use for this job" default:"2"` - Inputs []string `long:"input" description:"specify one or more inputs that will be used for the job using the following format: :"` - Outputs []string `long:"output" description:"specify one or more output folders that will be stored as datasets after the job is finished using the following format: :"` - Private bool `long:"private" description:"use this flag with a private image, a prompt will ask for your username and password. If DOCKER_USERNAME and/or DOCKER_PASSWORD are provided, they will be used as values to populate the registry secret."` + Name string `long:"name" short:"n" description:"assign a name to the job"` + Env []string `long:"env" short:"e" description:"environment variables to use"` + Memory string `long:"memory" short:"m" description:"memory to use for this job, expressed in gigabytes" default:"3"` + VCPU string `long:"vcpu" description:"number of vcpus to use for this job" default:"2"` + Inputs []string `long:"input" description:"specify one or more inputs that will be used for the job using the following format: :"` + Outputs []string `long:"output" description:"specify one or more output folders that will be stored as datasets after the job is finished using the following format: :"` + Private bool `long:"private" description:"use this flag with a private image, a prompt will ask for your username and password. If DOCKER_USERNAME and/or DOCKER_PASSWORD are provided, they will be used as values to populate the registry secret."` + CleanCreds bool `long:"clean-creds" description:"to be used with the '--private' flag, a prompt will ask again for your username and password. If DOCKER_USERNAME and/or DOCKER_PASSWORD are provided, they will be used as values to update the secret."` *command } @@ -262,26 +263,31 @@ func (cmd *JobRun) Execute(args []string) (err error) { VCPU: cmd.VCPU, } if cmd.Private { - username, password, err := cmd.getCredentials() - if err != nil { - return err - } - secrets, err := kube.ListSecrets(ctx, &svc.ListSecretsInput{}) if err != nil { return renderServiceError(err, "failed to list secrets") } for _, secret := range secrets.Items { if secret.Details.Image == in.Image { - _, err = kube.UpdateSecret(ctx, &svc.UpdateSecretInput{Name: secret.Name, Username: username, Password: password}) - if err != nil { - return renderServiceError(err, "failed to update secret") + if cmd.CleanCreds { + username, password, err := cmd.getCredentials() + if err != nil { + return err + } + _, err = kube.UpdateSecret(ctx, &svc.UpdateSecretInput{Name: secret.Name, Username: username, Password: password}) + if err != nil { + return renderServiceError(err, "failed to update secret") + } } in.Secret = secret.Name break } } if in.Secret == "" { + username, password, err := cmd.getCredentials() + if err != nil { + return err + } secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{ Image: in.Image, Username: username, diff --git a/spec.json b/spec.json index 746636d49..6b435d259 100644 --- a/spec.json +++ b/spec.json @@ -437,6 +437,12 @@ "description": "use this flag with a private image, a prompt will ask for your username and password. If DOCKER_USERNAME and/or DOCKER_PASSWORD are provided, they will be used as values to populate the registry secret.", "default_value": null, "choices": null + }, + { + "long_name": "clean-creds", + "description": "to be used with the '--private' flag, a prompt will ask again for your username and password. If DOCKER_USERNAME and/or DOCKER_PASSWORD are provided, they will be used as values to update the secret.", + "default_value": null, + "choices": null } ] } From 4512796bc2121154a32dbd18fe4466b1a7cea5a7 Mon Sep 17 00:00:00 2001 From: advanderveer Date: Fri, 23 Mar 2018 09:21:59 +0100 Subject: [PATCH 8/9] added more explicit documentation and prompt messages --- cmd/job_run.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/job_run.go b/cmd/job_run.go index 535beb6c4..bc4a34693 100755 --- a/cmd/job_run.go +++ b/cmd/job_run.go @@ -25,8 +25,8 @@ type JobRun struct { VCPU string `long:"vcpu" description:"number of vcpus to use for this job" default:"2"` Inputs []string `long:"input" description:"specify one or more inputs that will be used for the job using the following format: :"` Outputs []string `long:"output" description:"specify one or more output folders that will be stored as datasets after the job is finished using the following format: :"` - Private bool `long:"private" description:"use this flag with a private image, a prompt will ask for your username and password. If DOCKER_USERNAME and/or DOCKER_PASSWORD are provided, they will be used as values to populate the registry secret."` - CleanCreds bool `long:"clean-creds" description:"to be used with the '--private' flag, a prompt will ask again for your username and password. If DOCKER_USERNAME and/or DOCKER_PASSWORD are provided, they will be used as values to update the secret."` + Private bool `long:"private" description:"use this flag with a private image, a prompt will ask for your username and password of the repository that stores the image. If NERD_IMAGE_USERNAME and/or NERD_IMAGE_PASSWORD environment variables are set, those values are used instead."` + CleanCreds bool `long:"clean-creds" description:"to be used with the '--private' flag, a prompt will ask again for your image repository username and password. If NERD_IMAGE_USERNAME and/or NERD_IMAGE_PASSWORD environment variables are provided, they will be used as values to update the secret."` *command } @@ -358,14 +358,15 @@ func (cmd *JobRun) rollbackDatasets(ctx context.Context, mgr transfer.Manager, i } func (cmd *JobRun) getCredentials() (username, password string, err error) { - username = os.Getenv("DOCKER_USERNAME") + cmd.out.Infof("Please provide credentials for the Docker repository that stores the private image:") + username = os.Getenv("NERD_IMAGE_USERNAME") if username == "" { username, err = cmd.out.Ask("Username: ") if err != nil { return username, password, err } } - password = os.Getenv("DOCKER_PASSWORD") + password = os.Getenv("NERD_IMAGE_PASSWORD") if password == "" { password, err = cmd.out.AskSecret("Password: ") if err != nil { From 2e86a72da422742f5843e4131545ad486bd342bd Mon Sep 17 00:00:00 2001 From: Emeline Gaulard Date: Fri, 23 Mar 2018 09:42:06 +0100 Subject: [PATCH 9/9] Update tests and secret management --- cmd/job_run.go | 12 ++++++++---- spec.json | 4 ++-- svc/kube_create_secret.go | 14 ++++++++------ svc/kube_create_secret_test.go | 2 +- svc/kube_delete_secret_test.go | 3 +-- svc/kube_get_secret_test.go | 2 +- svc/kube_list_secrets_test.go | 4 ++-- svc/kube_update_secret_test.go | 4 +++- 8 files changed, 26 insertions(+), 19 deletions(-) diff --git a/cmd/job_run.go b/cmd/job_run.go index bc4a34693..33ead717c 100755 --- a/cmd/job_run.go +++ b/cmd/job_run.go @@ -267,10 +267,11 @@ func (cmd *JobRun) Execute(args []string) (err error) { if err != nil { return renderServiceError(err, "failed to list secrets") } + _, _, registry := svc.ExtractRegistry(in.Image) for _, secret := range secrets.Items { if secret.Details.Image == in.Image { if cmd.CleanCreds { - username, password, err := cmd.getCredentials() + username, password, err := cmd.getCredentials(registry) if err != nil { return err } @@ -284,7 +285,7 @@ func (cmd *JobRun) Execute(args []string) (err error) { } } if in.Secret == "" { - username, password, err := cmd.getCredentials() + username, password, err := cmd.getCredentials(registry) if err != nil { return err } @@ -357,8 +358,11 @@ func (cmd *JobRun) rollbackDatasets(ctx context.Context, mgr transfer.Manager, i return err } -func (cmd *JobRun) getCredentials() (username, password string, err error) { - cmd.out.Infof("Please provide credentials for the Docker repository that stores the private image:") +func (cmd *JobRun) getCredentials(registry string) (username, password string, err error) { + if registry == "index.docker.io" { + registry = "Docker Hub" + } + cmd.out.Infof("Please provide credentials for the %s repository that stores the private image:", registry) username = os.Getenv("NERD_IMAGE_USERNAME") if username == "" { username, err = cmd.out.Ask("Username: ") diff --git a/spec.json b/spec.json index 6b435d259..1ef01fc49 100644 --- a/spec.json +++ b/spec.json @@ -434,13 +434,13 @@ }, { "long_name": "private", - "description": "use this flag with a private image, a prompt will ask for your username and password. If DOCKER_USERNAME and/or DOCKER_PASSWORD are provided, they will be used as values to populate the registry secret.", + "description": "use this flag with a private image, a prompt will ask for your username and password of the repository that stores the image. If NERD_IMAGE_USERNAME and/or NERD_IMAGE_PASSWORD environment variables are set, those values are used instead.", "default_value": null, "choices": null }, { "long_name": "clean-creds", - "description": "to be used with the '--private' flag, a prompt will ask again for your username and password. If DOCKER_USERNAME and/or DOCKER_PASSWORD are provided, they will be used as values to update the secret.", + "description": "to be used with the '--private' flag, a prompt will ask again for your image repository username and password. If NERD_IMAGE_USERNAME and/or NERD_IMAGE_PASSWORD environment variables are provided, they will be used as values to update the secret.", "default_value": null, "choices": null } diff --git a/svc/kube_create_secret.go b/svc/kube_create_secret.go index 19c80d9b2..4926dea2a 100644 --- a/svc/kube_create_secret.go +++ b/svc/kube_create_secret.go @@ -17,8 +17,10 @@ import ( //CreateSecretInput is the input to CreateSecret type CreateSecretInput struct { Image string `validate:"printascii"` - Username string - Password string + Registry string `validate:"required"` + Project string + Username string `validate:"required"` + Password string `validate:"required"` } //CreateSecretOutput is the output to CreateSecret @@ -32,16 +34,15 @@ func (k *Kube) CreateSecret(ctx context.Context, in *CreateSecretInput) (out *Cr return nil, err } - image, project, registry := extractRegistry(in.Image) secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"image": image, "project": project, "registry": registry}, + Labels: map[string]string{"image": in.Image, "project": in.Project, "registry": in.Registry}, }, Type: v1.SecretTypeDockerConfigJson, Data: map[string][]byte{}, } - secret.Data[v1.DockerConfigJsonKey], err = transformCredentials(in.Username, in.Password, registry) + secret.Data[v1.DockerConfigJsonKey], err = transformCredentials(in.Username, in.Password, in.Registry) if err != nil { return nil, err } @@ -78,7 +79,8 @@ func transformCredentials(username, password, registry string) (dockereCfg []byt return dockerCfg, nil } -func extractRegistry(image string) (string, string, string) { +// ExtractRegistry takes a string as input and divides it in image, project, registry +func ExtractRegistry(image string) (string, string, string) { // Supported registries: // - project/image -> index.docker.io // - ACCOUNT.dkr.ecr.REGION.amazonaws.com/image -> aws diff --git a/svc/kube_create_secret_test.go b/svc/kube_create_secret_test.go index 5028f2c83..632cd3bde 100644 --- a/svc/kube_create_secret_test.go +++ b/svc/kube_create_secret_test.go @@ -32,7 +32,7 @@ func TestCreateSecret(t *testing.T) { { Name: "when a valid input is provided it should return a secret with a unique name", Timeout: time.Second * 5, - Input: &svc.CreateSecretInput{Image: "quay.io/nerdalize/smoketest", Username: "test", Password: "test"}, + Input: &svc.CreateSecretInput{Image: "smoketest", Project: "nerdalize", Registry: "quay.io", Username: "test", Password: "test"}, IsErr: nil, IsOutput: func(t testing.TB, out *svc.CreateSecretOutput) { assert(t, out != nil, "output should not be nil") diff --git a/svc/kube_delete_secret_test.go b/svc/kube_delete_secret_test.go index 7a9297bab..bd95fe011 100644 --- a/svc/kube_delete_secret_test.go +++ b/svc/kube_delete_secret_test.go @@ -68,7 +68,6 @@ func TestDeleteSecret(t *testing.T) { } func TestDeleteSpecificSecret(t *testing.T) { - image := "quay.io/nerdalize/smoketest" timeout := time.Minute if testing.Short() { @@ -83,7 +82,7 @@ func TestDeleteSpecificSecret(t *testing.T) { defer cancel() kube := svc.NewKube(di) - secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{Image: image, Username: "test", Password: "test"}) + secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{Image: "smoketest", Project: "nerdalize", Registry: "quay.io", Username: "test", Password: "test"}) ok(t, err) out, err := kube.DeleteSecret(ctx, &svc.DeleteSecretInput{Name: secret.Name}) diff --git a/svc/kube_get_secret_test.go b/svc/kube_get_secret_test.go index f141faee3..e4a7a887c 100644 --- a/svc/kube_get_secret_test.go +++ b/svc/kube_get_secret_test.go @@ -87,7 +87,7 @@ func TestGetSpecificSecret(t *testing.T) { defer cancel() kube := svc.NewKube(di) - secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{Image: image, Username: "test", Password: "test"}) + secret, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{Image: "smoketest", Project: "nerdalize", Registry: "quay.io", Username: "test", Password: "test"}) ok(t, err) out, err := kube.GetSecret(ctx, &svc.GetSecretInput{Name: secret.Name}) diff --git a/svc/kube_list_secrets_test.go b/svc/kube_list_secrets_test.go index 413ee3b82..e4c19cfd2 100644 --- a/svc/kube_list_secrets_test.go +++ b/svc/kube_list_secrets_test.go @@ -44,13 +44,13 @@ func TestListSecrets(t *testing.T) { { Name: "when one correct secret was created it should be listed", Timeout: time.Minute, - Secrets: []*svc.CreateSecretInput{{Image: "quay.io/nerdalize/smoketest", Username: "test", Password: "test"}}, + Secrets: []*svc.CreateSecretInput{{Image: "smoketest", Project: "nerdalize", Registry: "quay.io", Username: "test", Password: "test"}}, Input: &svc.ListSecretsInput{}, IsErr: isNilErr, IsOutput: func(t testing.TB, out *svc.ListSecretsOutput) bool { assert(t, len(out.Items) == 1, "expected one secret to be listed") assert(t, !out.Items[0].Details.CreatedAt.IsZero(), "created at time should not be zero") - + assert(t, out.Items[0].Details.Image == "quay.io/nerdalize/smoketest", "expected to find complete image name") assert(t, strings.HasPrefix(out.Items[0].Name, "s-"), "expected secret name to be prefixed has expected") return true }, diff --git a/svc/kube_update_secret_test.go b/svc/kube_update_secret_test.go index 5c8b787e1..a9a94b4fc 100644 --- a/svc/kube_update_secret_test.go +++ b/svc/kube_update_secret_test.go @@ -18,7 +18,9 @@ func TestUpdateSecret(t *testing.T) { kube := svc.NewKube(di) out, err := kube.CreateSecret(ctx, &svc.CreateSecretInput{ - Image: "quay.io/nerdalize/smoketest", + Image: "smoketest", + Project: "nerdalize", + Registry: "quay.io", Username: "test", Password: "test", })