diff --git a/cmd/job_run.go b/cmd/job_run.go
index 9fda6fc1a..33ead717c 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"
@@ -18,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: :"`
-
+ 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 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
}
@@ -260,6 +262,44 @@ func (cmd *JobRun) Execute(args []string) (err error) {
Memory: fmt.Sprintf("%sGi", cmd.Memory),
VCPU: cmd.VCPU,
}
+ if cmd.Private {
+ secrets, err := kube.ListSecrets(ctx, &svc.ListSecretsInput{})
+ 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(registry)
+ 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(registry)
+ 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)
@@ -318,6 +358,28 @@ func (cmd *JobRun) rollbackDatasets(ctx context.Context, mgr transfer.Manager, i
return err
}
+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: ")
+ if err != nil {
+ return username, password, err
+ }
+ }
+ password = os.Getenv("NERD_IMAGE_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/pkg/kubevisor/visor.go b/pkg/kubevisor/visor.go
index ca84c1680..fabe42989 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
@@ -112,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:
@@ -146,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)
}
@@ -212,6 +218,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-"
@@ -262,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()
@@ -303,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/spec.json b/spec.json
index c54f63b0f..88b8a647b 100644
--- a/spec.json
+++ b/spec.json
@@ -443,6 +443,18 @@
"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 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 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
new file mode 100644
index 000000000..4926dea2a
--- /dev/null
+++ b/svc/kube_create_secret.go
@@ -0,0 +1,103 @@
+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"`
+ Registry string `validate:"required"`
+ Project string
+ Username string `validate:"required"`
+ Password string `validate:"required"`
+}
+
+//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
+ }
+
+ secret := &v1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ 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, in.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{}{
+ "auths": auths,
+ "HttpHeaders": map[string]interface{}{
+ "User-Agent": "Docker-Client/1.11.2 (linux)",
+ },
+ }
+ authStr := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, 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 dockerCfg, errors.Wrap(err, "failed to serialize docker secret cfg")
+ }
+ return dockerCfg, nil
+}
+
+// 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
+ // - 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_create_secret_test.go b/svc/kube_create_secret_test.go
new file mode 100644
index 000000000..632cd3bde
--- /dev/null
+++ b/svc/kube_create_secret_test.go
@@ -0,0 +1,62 @@
+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: "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")
+ 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)
+ }
+ })
+ }
+}
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_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..bd95fe011
--- /dev/null
+++ b/svc/kube_delete_secret_test.go
@@ -0,0 +1,91 @@
+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) {
+ 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: "smoketest", Project: "nerdalize", Registry: "quay.io", 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_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
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..e4a7a887c
--- /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: "smoketest", Project: "nerdalize", Registry: "quay.io", 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..e4c19cfd2
--- /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: "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
+ },
+ },
+ } {
+ 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_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
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..a9a94b4fc
--- /dev/null
+++ b/svc/kube_update_secret_test.go
@@ -0,0 +1,35 @@
+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: "smoketest",
+ Project: "nerdalize",
+ Registry: "quay.io",
+ Username: "test",
+ Password: "test",
+ })
+ ok(t, err)
+
+ _, err = kube.UpdateSecret(ctx, &svc.UpdateSecretInput{
+ Name: out.Name,
+ Username: "newtest",
+ Password: "newtest",
+ })
+ ok(t, err)
+}