diff --git a/docs/architecture.rst b/docs/architecture.rst index bbdc538a4e..17bb4f0f43 100644 --- a/docs/architecture.rst +++ b/docs/architecture.rst @@ -153,6 +153,7 @@ as follows: Secrets map[string]ObjectReference `json:"secrets"` Options map[string]string `json:"options"` Profile *ObjectReference `json:"profile"` + PodOverride map[string]interface{} `json:"podOverride,omitempty"` } - ``Name`` is required and specifies the action in the Blueprint. @@ -168,6 +169,8 @@ as follows: - ``Profile`` is a reference to a :ref:`Profile` Kubernetes CustomResource that will be made available to the Blueprint. - ``Options`` is used to specify additional values to be used in the Blueprint +- ``PodOverride`` is used to specify pod specs that will override default specs + of the Pod created while executing functions like KubeTask, PrepareData, etc. As a reference, below is an example of a ActionSpec. diff --git a/docs/functions.rst b/docs/functions.rst index f14e984b6d..17112a97a9 100644 --- a/docs/functions.rst +++ b/docs/functions.rst @@ -121,6 +121,7 @@ This allows you to run a new Pod from a Blueprint. `namespace`, Yes, `string`, namespace in which to execute `image`, Yes, `string`, image to be used for executing the task `command`, Yes, `[]string`, command list to execute + `podOverride`, No, `map[string]interface{}`, specs to override default pod specs with Example: @@ -132,6 +133,10 @@ Example: args: namespace: "{{ .Deployment.Namespace }}" image: busybox + podOverride: + containers: + - name: container + imagePullPolicy: IfNotPresent command: - sh - -c @@ -225,6 +230,7 @@ ScaleWorkload. `volumes`, No, `map[string]string`, Mapping of ``pvcName`` to ``mountPath`` under which the volume will be available. `command`, Yes, `[]string`, command list to execute `serviceaccount`, No, `string`, service account info + `podOverride`, No, `map[string]interface{}`, specs to override default pod specs with .. note:: The ``volumes`` argument does not support ``subPath`` mounts so the @@ -407,6 +413,7 @@ and restores data to the specified path. `pod`, No, `string`, pod to which the volumes are attached `volumes`, No, `map[string]string`, Mapping of `pvcName` to `mountPath` under which the volume will be available `encryptionKey`, No, `string`, encryption key to be used during backups + `podOverride`, No, `map[string]interface{}`, specs to override default pod specs with .. note:: The ``image`` argument requires the use of ``kanisterio/kanister-tools`` @@ -481,6 +488,7 @@ respective PVCs and restores data to the specified path. `pods`, No, `string`, pods to which the volumes are attached `encryptionKey`, No, `string`, encryption key to be used during backups `backupInfo`, Yes, `string`, snapshot info generated as output in BackupDataAll function + `podOverride`, No, `map[string]interface{}`, specs to override default pod specs with .. note:: The `image` argument requires the use of `kanisterio/kanister-tools` @@ -549,6 +557,7 @@ Arguments: `volume`, Yes, `string`, name of the source PVC `dataArtifactPrefix`, Yes, `string`, path on the object store to store the data in `encryptionKey`, No, `string`, encryption key to be used during backups + `podOverride`, No, `map[string]interface{}`, specs to override default pod specs with Outputs: @@ -591,6 +600,7 @@ This function deletes the snapshot data backed up by the BackupData function. `backupIdentifier`, No, `string`, (required if backupTag not provided) unique snapshot id generated during backup `backupTag`, No, `string`, (required if backupIdentifier not provided) unique tag added during the backup `encryptionKey`, No, `string`, encryption key to be used during backups + `podOverride`, No, `map[string]interface{}`, specs to override default pod specs with Example: @@ -625,6 +635,7 @@ BackupDataAll function. `backupInfo`, Yes, `string`, snapshot info generated as output in BackupDataAll function `encryptionKey`, No, `string`, encryption key to be used during backups `reclaimSpace`, No, `bool`, provides a way to specify if space should be reclaimed + `podOverride`, No, `map[string]interface{}`, specs to override default pod specs with Example: diff --git a/docs/templates.rst b/docs/templates.rst index afde75f4f6..559cd0e8c3 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -24,6 +24,7 @@ The TemplateParam struct is defined as: Options map[string]string Object map[string]interface{} Phases map[string]*Phase + PodOverride map[string]interface{} } Rendering Templates diff --git a/go.mod b/go.mod index 99d58c656a..c6878d597c 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jarcoal/httpmock v1.0.4 // indirect github.com/jpillora/backoff v0.0.0-20170918002102-8eab2debe79d - github.com/json-iterator/go v1.1.6 // indirect + github.com/json-iterator/go v1.1.6 github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/kubernetes-csi/external-snapshotter v1.1.0 github.com/luci/go-render v0.0.0-20160219211803-9a04cc21af0f diff --git a/pkg/apis/cr/v1alpha1/types.go b/pkg/apis/cr/v1alpha1/types.go index 347688e80c..66cadb8b8d 100644 --- a/pkg/apis/cr/v1alpha1/types.go +++ b/pkg/apis/cr/v1alpha1/types.go @@ -22,11 +22,10 @@ which also has the apache 2.0 license. package v1alpha1 import ( - sp "k8s.io/apimachinery/pkg/util/strategicpatch" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + sp "k8s.io/apimachinery/pkg/util/strategicpatch" ) const ( @@ -100,7 +99,7 @@ type ActionSpec struct { // Profile is use to specify the location where store artifacts and the // credentials authorized to access them. Profile *ObjectReference `json:"profile"` - // PodOverride is use to specify pod specs that will override the + // PodOverride is used to specify pod specs that will override the // default pod specs PodOverride sp.JSONMap `json:"podOverride,omitempty"` // Options will be used to specify additional values diff --git a/pkg/function/args.go b/pkg/function/args.go index af4190cf83..d0cf4553bf 100644 --- a/pkg/function/args.go +++ b/pkg/function/args.go @@ -17,6 +17,10 @@ package function import ( "github.com/mitchellh/mapstructure" "github.com/pkg/errors" + sp "k8s.io/apimachinery/pkg/util/strategicpatch" + + "github.com/kanisterio/kanister/pkg/kube" + "github.com/kanisterio/kanister/pkg/param" ) // Arg returns the value of the specified argument @@ -45,3 +49,22 @@ func ArgExists(args map[string]interface{}, argName string) bool { _, ok := args[argName] return ok } + +// GetPodSpecOverride merges PodOverride specs passed in args and TemplateParams and returns combined Override specs +func GetPodSpecOverride(tp param.TemplateParams, args map[string]interface{}, argName string) (sp.JSONMap, error) { + var podOverride sp.JSONMap + var err error + if err = OptArg(args, KubeTaskPodOverrideArg, &podOverride, tp.PodOverride); err != nil { + return nil, err + } + + // Check if PodOverride specs are passed through actionset + // If yes, override podOverride specs + if tp.PodOverride != nil { + podOverride, err = kube.CreateAndMergeJsonPatch(podOverride, tp.PodOverride) + if err != nil { + return nil, err + } + } + return podOverride, nil +} diff --git a/pkg/function/copy_volume_data.go b/pkg/function/copy_volume_data.go index 3c900ad7a7..9a3212b784 100644 --- a/pkg/function/copy_volume_data.go +++ b/pkg/function/copy_volume_data.go @@ -43,6 +43,7 @@ const ( CopyVolumeDataOutputBackupArtifactLocation = "backupArtifactLocation" CopyVolumeDataEncryptionKeyArg = "encryptionKey" CopyVolumeDataOutputBackupTag = "backupTag" + CopyVolumeDataPodOverrideArg = "podOverride" ) func init() { @@ -57,7 +58,7 @@ func (*copyVolumeDataFunc) Name() string { return "CopyVolumeData" } -func copyVolumeData(ctx context.Context, cli kubernetes.Interface, tp param.TemplateParams, namespace, pvc, targetPath, encryptionKey string) (map[string]interface{}, error) { +func copyVolumeData(ctx context.Context, cli kubernetes.Interface, tp param.TemplateParams, namespace, pvc, targetPath, encryptionKey string, podOverride map[string]interface{}) (map[string]interface{}, error) { // Validate PVC exists if _, err := cli.CoreV1().PersistentVolumeClaims(namespace).Get(pvc, metav1.GetOptions{}); err != nil { return nil, errors.Wrapf(err, "Failed to retrieve PVC. Namespace %s, Name %s", namespace, pvc) @@ -70,6 +71,7 @@ func copyVolumeData(ctx context.Context, cli kubernetes.Interface, tp param.Temp Image: kanisterToolsImage, Command: []string{"sh", "-c", "tail -f /dev/null"}, Volumes: map[string]string{pvc: mountPoint}, + PodOverride: podOverride, } pr := kube.NewPodRunner(cli, options) podFunc := copyVolumeDataPodFunc(cli, tp, namespace, mountPoint, targetPath, encryptionKey) @@ -133,11 +135,16 @@ func (*copyVolumeDataFunc) Exec(ctx context.Context, tp param.TemplateParams, ar if err = OptArg(args, CopyVolumeDataEncryptionKeyArg, &encryptionKey, restic.GeneratePassword()); err != nil { return nil, err } + podOverride, err := GetPodSpecOverride(tp, args, CopyVolumeDataPodOverrideArg) + if err != nil { + return nil, err + } + cli, err := kube.NewClient() if err != nil { return nil, errors.Wrapf(err, "Failed to create Kubernetes client") } - return copyVolumeData(ctx, cli, tp, namespace, vol, targetPath, encryptionKey) + return copyVolumeData(ctx, cli, tp, namespace, vol, targetPath, encryptionKey, podOverride) } func (*copyVolumeDataFunc) RequiredArgs() []string { diff --git a/pkg/function/delete_data.go b/pkg/function/delete_data.go index 1d90ce2000..8fd587575a 100644 --- a/pkg/function/delete_data.go +++ b/pkg/function/delete_data.go @@ -20,6 +20,7 @@ import ( "github.com/pkg/errors" v1 "k8s.io/api/core/v1" + sp "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/kubernetes" kanister "github.com/kanisterio/kanister/pkg" @@ -42,7 +43,9 @@ const ( DeleteDataEncryptionKeyArg = "encryptionKey" // DeleteDataReclaimSpace provides a way to specify if space should be reclaimed DeleteDataReclaimSpace = "reclaimSpace" - deleteDataJobPrefix = "delete-data-" + // DeleteDataPodOverrideArg contains pod specs to override default pod specs + DeleteDataPodOverrideArg = "podOverride" + deleteDataJobPrefix = "delete-data-" ) func init() { @@ -57,12 +60,13 @@ func (*deleteDataFunc) Name() string { return "DeleteData" } -func deleteData(ctx context.Context, cli kubernetes.Interface, tp param.TemplateParams, reclaimSpace bool, namespace, encryptionKey string, targetPaths, deleteTags, deleteIdentifiers []string, jobPrefix string) (map[string]interface{}, error) { +func deleteData(ctx context.Context, cli kubernetes.Interface, tp param.TemplateParams, reclaimSpace bool, namespace, encryptionKey string, targetPaths, deleteTags, deleteIdentifiers []string, jobPrefix string, podOverride sp.JSONMap) (map[string]interface{}, error) { options := &kube.PodOptions{ Namespace: namespace, GenerateName: jobPrefix, Image: kanisterToolsImage, Command: []string{"sh", "-c", "tail -f /dev/null"}, + PodOverride: podOverride, } pr := kube.NewPodRunner(cli, options) podFunc := deleteDataPodFunc(cli, tp, reclaimSpace, namespace, encryptionKey, targetPaths, deleteTags, deleteIdentifiers) @@ -156,6 +160,11 @@ func (*deleteDataFunc) Exec(ctx context.Context, tp param.TemplateParams, args m if err = OptArg(args, DeleteDataReclaimSpace, &reclaimSpace, false); err != nil { return nil, err } + podOverride, err := GetPodSpecOverride(tp, args, DeleteDataPodOverrideArg) + if err != nil { + return nil, err + } + // Validate profile if err = validateProfile(tp.Profile); err != nil { return nil, err @@ -164,7 +173,7 @@ func (*deleteDataFunc) Exec(ctx context.Context, tp param.TemplateParams, args m if err != nil { return nil, errors.Wrapf(err, "Failed to create Kubernetes client") } - return deleteData(ctx, cli, tp, reclaimSpace, namespace, encryptionKey, strings.Fields(deleteArtifactPrefix), strings.Fields(deleteTag), strings.Fields(deleteIdentifier), deleteDataJobPrefix) + return deleteData(ctx, cli, tp, reclaimSpace, namespace, encryptionKey, strings.Fields(deleteArtifactPrefix), strings.Fields(deleteTag), strings.Fields(deleteIdentifier), deleteDataJobPrefix, podOverride) } func (*deleteDataFunc) RequiredArgs() []string { diff --git a/pkg/function/delete_data_all.go b/pkg/function/delete_data_all.go index 428a48e57d..f2ff5daf0a 100644 --- a/pkg/function/delete_data_all.go +++ b/pkg/function/delete_data_all.go @@ -38,7 +38,9 @@ const ( DeleteDataAllReclaimSpace = "reclaimSpace" // DeleteDataAllBackupInfo provides backup info required for delete DeleteDataAllBackupInfo = "backupInfo" - deleteDataAllJobPrefix = "delete-data-all-" + // DeleteDataAllPodOverrideArg contains pod specs to override default pod specs + DeleteDataAllPodOverrideArg = "podOverride" + deleteDataAllJobPrefix = "delete-data-all-" ) func init() { @@ -72,6 +74,11 @@ func (*deleteDataAllFunc) Exec(ctx context.Context, tp param.TemplateParams, arg if err = OptArg(args, DeleteDataAllReclaimSpace, &reclaimSpace, false); err != nil { return nil, err } + podOverride, err := GetPodSpecOverride(tp, args, DeleteDataAllPodOverrideArg) + if err != nil { + return nil, err + } + // Validate profile if err = validateProfile(tp.Profile); err != nil { return nil, err @@ -92,7 +99,7 @@ func (*deleteDataAllFunc) Exec(ctx context.Context, tp param.TemplateParams, arg deleteIdentifiers = append(deleteIdentifiers, info.BackupID) } - return deleteData(ctx, cli, tp, reclaimSpace, namespace, encryptionKey, targetPaths, nil, deleteIdentifiers, deleteDataAllJobPrefix) + return deleteData(ctx, cli, tp, reclaimSpace, namespace, encryptionKey, targetPaths, nil, deleteIdentifiers, deleteDataAllJobPrefix, podOverride) } func (*deleteDataAllFunc) RequiredArgs() []string { diff --git a/pkg/function/kube_task.go b/pkg/function/kube_task.go index fdf853f29b..51fdc13794 100644 --- a/pkg/function/kube_task.go +++ b/pkg/function/kube_task.go @@ -19,6 +19,7 @@ import ( "github.com/pkg/errors" "k8s.io/api/core/v1" + sp "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/kubernetes" kanister "github.com/kanisterio/kanister/pkg" @@ -30,10 +31,11 @@ import ( ) const ( - jobPrefix = "kanister-job-" - KubeTaskNamespaceArg = "namespace" - KubeTaskImageArg = "image" - KubeTaskCommandArg = "command" + jobPrefix = "kanister-job-" + KubeTaskNamespaceArg = "namespace" + KubeTaskImageArg = "image" + KubeTaskCommandArg = "command" + KubeTaskPodOverrideArg = "podOverride" ) func init() { @@ -48,7 +50,7 @@ func (*kubeTaskFunc) Name() string { return "KubeTask" } -func kubeTask(ctx context.Context, cli kubernetes.Interface, namespace, image string, command []string) (map[string]interface{}, error) { +func kubeTask(ctx context.Context, cli kubernetes.Interface, namespace, image string, command []string, podOverride sp.JSONMap) (map[string]interface{}, error) { var serviceAccount string var err error if namespace == "" { @@ -67,7 +69,9 @@ func kubeTask(ctx context.Context, cli kubernetes.Interface, namespace, image st Image: image, Command: command, ServiceAccountName: serviceAccount, + PodOverride: podOverride, } + pr := kube.NewPodRunner(cli, options) podFunc := kubeTaskPodFunc(cli) return pr.Run(ctx, podFunc) @@ -109,11 +113,16 @@ func (ktf *kubeTaskFunc) Exec(ctx context.Context, tp param.TemplateParams, args if err = OptArg(args, KubeTaskNamespaceArg, &namespace, ""); err != nil { return nil, err } + podOverride, err := GetPodSpecOverride(tp, args, KubeTaskPodOverrideArg) + if err != nil { + return nil, err + } + cli, err := kube.NewClient() if err != nil { return nil, errors.Wrapf(err, "Failed to create Kubernetes client") } - return kubeTask(ctx, cli, namespace, image, command) + return kubeTask(ctx, cli, namespace, image, command, podOverride) } func (*kubeTaskFunc) RequiredArgs() []string { diff --git a/pkg/function/kube_task_test.go b/pkg/function/kube_task_test.go index af334461c2..112b0f715e 100644 --- a/pkg/function/kube_task_test.go +++ b/pkg/function/kube_task_test.go @@ -22,6 +22,7 @@ import ( . "gopkg.in/check.v1" "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + sp "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/kubernetes" kanister "github.com/kanisterio/kanister/pkg" @@ -126,6 +127,14 @@ func (s *KubeTaskSuite) TestKubeTask(c *C) { StatefulSet: ¶m.StatefulSetParams{ Namespace: s.namespace, }, + PodOverride: sp.JSONMap{ + "containers": []map[string]interface{}{ + { + "name": "container", + "imagePullPolicy": "Always", + }, + }, + }, } action := "test" for _, tc := range []struct { diff --git a/pkg/function/prepare_data.go b/pkg/function/prepare_data.go index 03e85b9906..090bf594cf 100644 --- a/pkg/function/prepare_data.go +++ b/pkg/function/prepare_data.go @@ -21,6 +21,7 @@ import ( "github.com/pkg/errors" "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + sp "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/kubernetes" kanister "github.com/kanisterio/kanister/pkg" @@ -37,6 +38,7 @@ const ( PrepareDataCommandArg = "command" PrepareDataVolumes = "volumes" PrepareDataServiceAccount = "serviceaccount" + PrepareDataPodOverrideArg = "podOverride" ) func init() { @@ -73,7 +75,7 @@ func getVolumes(tp param.TemplateParams) (map[string]string, error) { return vols, nil } -func prepareData(ctx context.Context, cli kubernetes.Interface, namespace, serviceAccount, image string, vols map[string]string, command ...string) (map[string]interface{}, error) { +func prepareData(ctx context.Context, cli kubernetes.Interface, namespace, serviceAccount, image string, vols map[string]string, podOverride sp.JSONMap, command ...string) (map[string]interface{}, error) { // Validate volumes for pvc := range vols { if _, err := cli.CoreV1().PersistentVolumeClaims(namespace).Get(pvc, metav1.GetOptions{}); err != nil { @@ -87,6 +89,7 @@ func prepareData(ctx context.Context, cli kubernetes.Interface, namespace, servi Command: command, Volumes: vols, ServiceAccountName: serviceAccount, + PodOverride: podOverride, } pr := kube.NewPodRunner(cli, options) podFunc := prepareDataPodFunc(cli) @@ -130,6 +133,11 @@ func (*prepareDataFunc) Exec(ctx context.Context, tp param.TemplateParams, args if err = OptArg(args, PrepareDataServiceAccount, &serviceAccount, ""); err != nil { return nil, err } + podOverride, err := GetPodSpecOverride(tp, args, PrepareDataPodOverrideArg) + if err != nil { + return nil, err + } + cli, err := kube.NewClient() if err != nil { return nil, errors.Wrapf(err, "Failed to create Kubernetes client") @@ -139,7 +147,7 @@ func (*prepareDataFunc) Exec(ctx context.Context, tp param.TemplateParams, args return nil, err } } - return prepareData(ctx, cli, namespace, serviceAccount, image, vols, command...) + return prepareData(ctx, cli, namespace, serviceAccount, image, vols, podOverride, command...) } func (*prepareDataFunc) RequiredArgs() []string { diff --git a/pkg/function/restore_data.go b/pkg/function/restore_data.go index 9afa7a1a35..7104c33dd3 100644 --- a/pkg/function/restore_data.go +++ b/pkg/function/restore_data.go @@ -20,6 +20,7 @@ import ( "github.com/pkg/errors" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + sp "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/kubernetes" kanister "github.com/kanisterio/kanister/pkg" @@ -49,6 +50,8 @@ const ( RestoreDataEncryptionKeyArg = "encryptionKey" // RestoreDataBackupTagArg provides a unique tag added to the backup artifacts RestoreDataBackupTagArg = "backupTag" + // RestoreDataPodOverrideArg contains pod specs which overrides default pod specs + RestoreDataPodOverrideArg = "podOverride" ) func init() { @@ -63,38 +66,44 @@ func (*restoreDataFunc) Name() string { return "RestoreData" } -func validateAndGetOptArgs(args map[string]interface{}) (string, string, string, map[string]string, string, string, error) { +func validateAndGetOptArgs(args map[string]interface{}, tp param.TemplateParams) (string, string, string, map[string]string, string, string, sp.JSONMap, error) { var restorePath, encryptionKey, pod, tag, id string var vols map[string]string + var podOverride sp.JSONMap var err error if err = OptArg(args, RestoreDataRestorePathArg, &restorePath, "/"); err != nil { - return restorePath, encryptionKey, pod, vols, tag, id, err + return restorePath, encryptionKey, pod, vols, tag, id, podOverride, err } if err = OptArg(args, RestoreDataEncryptionKeyArg, &encryptionKey, restic.GeneratePassword()); err != nil { - return restorePath, encryptionKey, pod, vols, tag, id, err + return restorePath, encryptionKey, pod, vols, tag, id, podOverride, err } if err = OptArg(args, RestoreDataPodArg, &pod, ""); err != nil { - return restorePath, encryptionKey, pod, vols, tag, id, err + return restorePath, encryptionKey, pod, vols, tag, id, podOverride, err } if err = OptArg(args, RestoreDataVolsArg, &vols, nil); err != nil { - return restorePath, encryptionKey, pod, vols, tag, id, err + return restorePath, encryptionKey, pod, vols, tag, id, podOverride, err } if (pod != "") == (len(vols) > 0) { - return restorePath, encryptionKey, pod, vols, tag, id, + return restorePath, encryptionKey, pod, vols, tag, id, podOverride, errors.Errorf("Require one argument: %s or %s", RestoreDataPodArg, RestoreDataVolsArg) } if err = OptArg(args, RestoreDataBackupTagArg, &tag, nil); err != nil { - return restorePath, encryptionKey, pod, vols, tag, id, err + return restorePath, encryptionKey, pod, vols, tag, id, podOverride, err } if err = OptArg(args, RestoreDataBackupIdentifierArg, &id, nil); err != nil { - return restorePath, encryptionKey, pod, vols, tag, id, err + return restorePath, encryptionKey, pod, vols, tag, id, podOverride, err } if (tag != "") == (id != "") { - return restorePath, encryptionKey, pod, vols, tag, id, + return restorePath, encryptionKey, pod, vols, tag, id, podOverride, errors.Errorf("Require one argument: %s or %s", RestoreDataBackupTagArg, RestoreDataBackupIdentifierArg) } - return restorePath, encryptionKey, pod, vols, tag, id, nil + podOverride, err = GetPodSpecOverride(tp, args, RestoreDataPodOverrideArg) + if err != nil { + return restorePath, encryptionKey, pod, vols, tag, id, podOverride, err + } + + return restorePath, encryptionKey, pod, vols, tag, id, podOverride, nil } func fetchPodVolumes(pod string, tp param.TemplateParams) (map[string]string, error) { @@ -114,7 +123,7 @@ func fetchPodVolumes(pod string, tp param.TemplateParams) (map[string]string, er } } -func restoreData(ctx context.Context, cli kubernetes.Interface, tp param.TemplateParams, namespace, encryptionKey, backupArtifactPrefix, restorePath, backupTag, backupID, jobPrefix string, vols map[string]string) (map[string]interface{}, error) { +func restoreData(ctx context.Context, cli kubernetes.Interface, tp param.TemplateParams, namespace, encryptionKey, backupArtifactPrefix, restorePath, backupTag, backupID, jobPrefix string, vols map[string]string, podOverride sp.JSONMap) (map[string]interface{}, error) { // Validate volumes for pvc := range vols { if _, err := cli.CoreV1().PersistentVolumeClaims(namespace).Get(pvc, metav1.GetOptions{}); err != nil { @@ -127,6 +136,7 @@ func restoreData(ctx context.Context, cli kubernetes.Interface, tp param.Templat Image: kanisterToolsImage, Command: []string{"sh", "-c", "tail -f /dev/null"}, Volumes: vols, + PodOverride: podOverride, } pr := kube.NewPodRunner(cli, options) podFunc := restoreDataPodFunc(cli, tp, namespace, encryptionKey, backupArtifactPrefix, restorePath, backupTag, backupID) @@ -167,6 +177,7 @@ func restoreDataPodFunc(cli kubernetes.Interface, tp param.TemplateParams, names func (*restoreDataFunc) Exec(ctx context.Context, tp param.TemplateParams, args map[string]interface{}) (map[string]interface{}, error) { var namespace, image, backupArtifactPrefix, backupTag, backupID string + var podOverride sp.JSONMap var err error if err = Arg(args, RestoreDataNamespaceArg, &namespace); err != nil { return nil, err @@ -177,11 +188,25 @@ func (*restoreDataFunc) Exec(ctx context.Context, tp param.TemplateParams, args if err = Arg(args, RestoreDataBackupArtifactPrefixArg, &backupArtifactPrefix); err != nil { return nil, err } + // Validate and get optional arguments - restorePath, encryptionKey, pod, vols, backupTag, backupID, err := validateAndGetOptArgs(args) + restorePath, encryptionKey, pod, vols, backupTag, backupID, podOverride, err := validateAndGetOptArgs(args, tp) if err != nil { return nil, err } + if podOverride == nil { + podOverride = tp.PodOverride + } + + // Check if PodOverride specs are passed through actionset + // If yes, override podOverride specs + if tp.PodOverride != nil { + podOverride, err = kube.CreateAndMergeJsonPatch(podOverride, tp.PodOverride) + if err != nil { + return nil, err + } + } + // Validate profile if err = validateProfile(tp.Profile); err != nil { return nil, err @@ -197,7 +222,7 @@ func (*restoreDataFunc) Exec(ctx context.Context, tp param.TemplateParams, args if err != nil { return nil, errors.Wrapf(err, "Failed to create Kubernetes client") } - return restoreData(ctx, cli, tp, namespace, encryptionKey, backupArtifactPrefix, restorePath, backupTag, backupID, restoreDataJobPrefix, vols) + return restoreData(ctx, cli, tp, namespace, encryptionKey, backupArtifactPrefix, restorePath, backupTag, backupID, restoreDataJobPrefix, vols, podOverride) } func (*restoreDataFunc) RequiredArgs() []string { diff --git a/pkg/function/restore_data_all.go b/pkg/function/restore_data_all.go index b458234e2f..c51b78194d 100644 --- a/pkg/function/restore_data_all.go +++ b/pkg/function/restore_data_all.go @@ -22,6 +22,8 @@ import ( "github.com/pkg/errors" + sp "k8s.io/apimachinery/pkg/util/strategicpatch" + kanister "github.com/kanisterio/kanister/pkg" "github.com/kanisterio/kanister/pkg/kube" "github.com/kanisterio/kanister/pkg/param" @@ -44,6 +46,8 @@ const ( RestoreDataAllEncryptionKeyArg = "encryptionKey" // RestoreDataAllBackupInfo provides backup info required for restore RestoreDataAllBackupInfo = "backupInfo" + // RestoreDataPodOverrideArg contains pod specs which overrides default pod specs + RestoreDataAllPodOverrideArg = "podOverride" ) func init() { @@ -58,19 +62,24 @@ func (*restoreDataAllFunc) Name() string { return "RestoreDataAll" } -func validateAndGetRestoreAllOptArgs(args map[string]interface{}, tp param.TemplateParams) (string, string, []string, error) { +func validateAndGetRestoreAllOptArgs(args map[string]interface{}, tp param.TemplateParams) (string, string, []string, sp.JSONMap, error) { var restorePath, encryptionKey, pods string var ps []string + var podOverride sp.JSONMap var err error if err = OptArg(args, RestoreDataAllRestorePathArg, &restorePath, "/"); err != nil { - return restorePath, encryptionKey, ps, err + return restorePath, encryptionKey, ps, podOverride, err } if err = OptArg(args, RestoreDataAllEncryptionKeyArg, &encryptionKey, restic.GeneratePassword()); err != nil { - return restorePath, encryptionKey, ps, err + return restorePath, encryptionKey, ps, podOverride, err } if err = OptArg(args, RestoreDataAllPodsArg, &pods, ""); err != nil { - return restorePath, encryptionKey, ps, err + return restorePath, encryptionKey, ps, podOverride, err + } + podOverride, err = GetPodSpecOverride(tp, args, RestoreDataAllPodOverrideArg) + if err != nil { + return restorePath, encryptionKey, ps, podOverride, err } if pods != "" { @@ -82,11 +91,11 @@ func validateAndGetRestoreAllOptArgs(args map[string]interface{}, tp param.Templ case tp.StatefulSet != nil: ps = tp.StatefulSet.Pods default: - return restorePath, encryptionKey, ps, errors.New("Unsupported workload type") + return restorePath, encryptionKey, ps, podOverride, errors.New("Unsupported workload type") } } - return restorePath, encryptionKey, ps, nil + return restorePath, encryptionKey, ps, podOverride, nil } func (*restoreDataAllFunc) Exec(ctx context.Context, tp param.TemplateParams, args map[string]interface{}) (map[string]interface{}, error) { @@ -104,11 +113,13 @@ func (*restoreDataAllFunc) Exec(ctx context.Context, tp param.TemplateParams, ar if err = Arg(args, RestoreDataAllBackupInfo, &backupInfo); err != nil { return nil, err } + // Validate and get optional arguments - restorePath, encryptionKey, pods, err := validateAndGetRestoreAllOptArgs(args, tp) + restorePath, encryptionKey, pods, podOverride, err := validateAndGetRestoreAllOptArgs(args, tp) if err != nil { return nil, err } + // Validate profile if err = validateProfile(tp.Profile); err != nil { return nil, err @@ -135,7 +146,7 @@ func (*restoreDataAllFunc) Exec(ctx context.Context, tp param.TemplateParams, ar outputChan <- out return } - out, err = restoreData(ctx, cli, tp, namespace, encryptionKey, fmt.Sprintf("%s/%s", backupArtifactPrefix, pod), restorePath, "", input[pod].BackupID, restoreDataAllJobPrefix, vols) + out, err = restoreData(ctx, cli, tp, namespace, encryptionKey, fmt.Sprintf("%s/%s", backupArtifactPrefix, pod), restorePath, "", input[pod].BackupID, restoreDataAllJobPrefix, vols, podOverride) errChan <- errors.Wrapf(err, "Failed to restore data for pod %s", pod) outputChan <- out }(pod) diff --git a/pkg/function/restore_data_test.go b/pkg/function/restore_data_test.go index 2ebdd4cd0c..f8d8c3adef 100644 --- a/pkg/function/restore_data_test.go +++ b/pkg/function/restore_data_test.go @@ -17,6 +17,8 @@ package function import ( . "gopkg.in/check.v1" + sp "k8s.io/apimachinery/pkg/util/strategicpatch" + "github.com/kanisterio/kanister/pkg/param" ) @@ -115,6 +117,7 @@ func (s *RestoreDataTestSuite) TestValidateAndGetOptArgs(c *C) { name string args map[string]interface{} errChecker Checker + tp param.TemplateParams }{ { name: "Args with Pod", @@ -171,9 +174,30 @@ func (s *RestoreDataTestSuite) TestValidateAndGetOptArgs(c *C) { }, errChecker: NotNil, }, + { + name: "Args with podOverride", + args: map[string]interface{}{ + RestoreDataPodArg: "some-pod", + RestoreDataBackupIdentifierArg: "backup123", + RestoreDataPodOverrideArg: sp.JSONMap{ + "containers": []map[string]interface{}{ + { + "name": "container", + "command": []string{"echo", "in unit tests"}, + }, + }, + }, + }, + errChecker: IsNil, + tp: param.TemplateParams{ + PodOverride: sp.JSONMap{ + "dnsPolicy": "ClusterFirst", + }, + }, + }, } for _, tc := range testCases { - _, _, _, _, _, _, err := validateAndGetOptArgs(tc.args) + _, _, _, _, _, _, _, err := validateAndGetOptArgs(tc.args, tc.tp) c.Check(err, tc.errChecker, Commentf("Case %s failed", tc.name)) } } diff --git a/pkg/kube/pod.go b/pkg/kube/pod.go index 31f1f53bf0..7eafda600e 100644 --- a/pkg/kube/pod.go +++ b/pkg/kube/pod.go @@ -19,10 +19,12 @@ import ( "io" "io/ioutil" + json "github.com/json-iterator/go" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + sp "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/kubernetes" "github.com/kanisterio/kanister/pkg/poll" @@ -36,35 +38,44 @@ type PodOptions struct { Command []string Volumes map[string]string ServiceAccountName string + PodOverride sp.JSONMap } // CreatePod creates a pod with a single container based on the specified image func CreatePod(ctx context.Context, cli kubernetes.Interface, opts *PodOptions) (*v1.Pod, error) { volumeMounts, podVolumes := createVolumeSpecs(opts.Volumes) + defaultSpecs := v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "container", + Image: opts.Image, + Command: opts.Command, + ImagePullPolicy: v1.PullPolicy(v1.PullAlways), + VolumeMounts: volumeMounts, + }, + }, + // RestartPolicy dictates when the containers of the pod should be restarted. + // The possible values include Always, OnFailure and Never with Always being the default. + // OnFailure policy will result in failed containers being restarted with an exponential back-off delay. + RestartPolicy: v1.RestartPolicyOnFailure, + Volumes: podVolumes, + ServiceAccountName: opts.ServiceAccountName, + } + + // Patch default Pod Specs if needed + patchedSpecs, err := patchDefaultPodSpecs(defaultSpecs, opts.PodOverride) + if err != nil { + return nil, errors.Wrapf(err, "Failed to create pod. Failed to override pod specs. Namespace: %s, NameFmt: %s", opts.Namespace, opts.GenerateName) + } + pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ GenerateName: opts.GenerateName, Namespace: opts.Namespace, }, - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "container", - Image: opts.Image, - Command: opts.Command, - ImagePullPolicy: v1.PullPolicy(v1.PullAlways), - VolumeMounts: volumeMounts, - }, - }, - // RestartPolicy dictates when the containers of the pod should be restarted. - // The possible values include Always, OnFailure and Never with Always being the default. - // OnFailure policy will result in failed containers being restarted with an exponential back-off delay. - RestartPolicy: v1.RestartPolicyOnFailure, - Volumes: podVolumes, - ServiceAccountName: opts.ServiceAccountName, - }, + Spec: patchedSpecs, } - pod, err := cli.CoreV1().Pods(opts.Namespace).Create(pod) + pod, err = cli.CoreV1().Pods(opts.Namespace).Create(pod) if err != nil { return nil, errors.Wrapf(err, "Failed to create pod. Namespace: %s, NameFmt: %s", opts.Namespace, opts.GenerateName) } @@ -133,3 +144,58 @@ func WaitForPodCompletion(ctx context.Context, cli kubernetes.Interface, namespa }) return errors.Wrap(err, "Pod did not transition into complete state") } + +// use Strategic Merge to patch default pod specs with the passed specs +func patchDefaultPodSpecs(defaultPodSpecs v1.PodSpec, override sp.JSONMap) (v1.PodSpec, error) { + // Merge default specs and override specs with StrategicMergePatch + mergedPatch, err := strategicMergeJsonPatch(defaultPodSpecs, override) + if err != nil { + return v1.PodSpec{}, err + } + + // Convert merged json to v1.PodSPec object + podSpec := v1.PodSpec{} + json.Unmarshal(mergedPatch, &podSpec) + if err != nil { + return podSpec, err + } + return podSpec, err +} + +// CreateAndMergeJsonPatch uses Strategic Merge to merge two Pod spec configuration +func CreateAndMergeJsonPatch(original, override sp.JSONMap) (sp.JSONMap, error) { + // Merge json specs with StrategicMerge + mergedPatch, err := strategicMergeJsonPatch(original, override) + if err != nil { + return nil, err + } + + // Convert merged json to map[string]interface{} + var merged map[string]interface{} + json.Unmarshal(mergedPatch, &merged) + if err != nil { + return nil, err + } + return merged, err +} + +func strategicMergeJsonPatch(original, override interface{}) ([]byte, error) { + // Convert override specs to json + overrideJson, err := json.Marshal(override) + if err != nil { + return nil, err + } + + // Convert original specs to json + originalJson, err := json.Marshal(original) + if err != nil { + return nil, err + } + + // Merge json specs with StrategicMerge + mergedPatch, err := sp.StrategicMergePatch(originalJson, overrideJson, v1.PodSpec{}) + if err != nil { + return nil, err + } + return mergedPatch, nil +} diff --git a/pkg/kube/pod_test.go b/pkg/kube/pod_test.go index fc6f1fd9af..c3d8b2b61b 100644 --- a/pkg/kube/pod_test.go +++ b/pkg/kube/pod_test.go @@ -26,6 +26,7 @@ import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + sp "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/testing" @@ -118,3 +119,393 @@ func (s *PodSuite) TestGetPodLogs(c *C) { c.Assert(strings.Contains(logs, "hello"), Equals, true) c.Assert(DeletePod(context.Background(), s.cli, pod), IsNil) } + +func (s *PodSuite) TestPatchDefaultPodSpecs(c *C) { + defaultSpecs := v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "container", + Image: "kanisterio/kanister-tools:0.21.0", + Command: []string{"sh", "-c", "echo in default specs"}, + ImagePullPolicy: v1.PullPolicy(v1.PullAlways), + VolumeMounts: []v1.VolumeMount{ + { + Name: "data", + MountPath: "/var/lib/data", + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyOnFailure, + Volumes: []v1.Volume{ + { + Name: "data", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "default-pvc", + }, + }, + }, + }, + } + + tests := []struct { + BlueprintPodSpecs sp.JSONMap + ActionsetPodSpecs sp.JSONMap + Expected v1.PodSpec + }{ + // Blueprint and Actionset PodOverride specs are nil + { + BlueprintPodSpecs: nil, + ActionsetPodSpecs: nil, + Expected: defaultSpecs, + }, + + // Blueprint PodOverride specs are nil + { + BlueprintPodSpecs: nil, + ActionsetPodSpecs: sp.JSONMap{ + "restartPolicy": "Always", + }, + Expected: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "container", + Image: "kanisterio/kanister-tools:0.21.0", + Command: []string{"sh", "-c", "echo in default specs"}, + ImagePullPolicy: v1.PullPolicy(v1.PullAlways), + VolumeMounts: []v1.VolumeMount{ + { + Name: "data", + MountPath: "/var/lib/data", + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyAlways, + Volumes: []v1.Volume{ + { + Name: "data", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "default-pvc", + }, + }, + }, + }, + }, + }, + + // Actionset PodOverride specs are nil + { + BlueprintPodSpecs: sp.JSONMap{ + "containers": []map[string]interface{}{ + { + "name": "container", + "imagePullPolicy": "IfNotPresent", + }, + }, + }, + ActionsetPodSpecs: nil, + Expected: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "container", + Image: "kanisterio/kanister-tools:0.21.0", + Command: []string{"sh", "-c", "echo in default specs"}, + ImagePullPolicy: v1.PullPolicy(v1.PullIfNotPresent), + VolumeMounts: []v1.VolumeMount{ + { + Name: "data", + MountPath: "/var/lib/data", + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyOnFailure, + Volumes: []v1.Volume{ + { + Name: "data", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "default-pvc", + }, + }, + }, + }, + }, + }, + + // Modify volume mounts + { + BlueprintPodSpecs: sp.JSONMap{ + "containers": []map[string]interface{}{ + { + "name": "container", + "volumeMounts": []map[string]interface{}{ + { + "mountPath": "/var/lib/other", + "name": "data", + }, + }, + }, + }, + }, + ActionsetPodSpecs: sp.JSONMap{ + "volumes": []map[string]interface{}{ + { + "name": "data", + "persistentVolumeClaim": map[string]interface{}{ + "claimName": "other-claim", + }, + }, + }, + }, + Expected: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "container", + Image: "kanisterio/kanister-tools:0.21.0", + Command: []string{"sh", "-c", "echo in default specs"}, + ImagePullPolicy: v1.PullPolicy(v1.PullAlways), + VolumeMounts: []v1.VolumeMount{ + { + Name: "data", + MountPath: "/var/lib/other", + }, + { + Name: "data", + MountPath: "/var/lib/data", + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyOnFailure, + Volumes: []v1.Volume{ + { + Name: "data", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "other-claim", + }, + }, + }, + }, + }, + }, + + // Add NodeSelector and Tolerations + { + BlueprintPodSpecs: sp.JSONMap{ + "nodeSelector": map[string]interface{}{ + "selector-key": "selector-value", + }, + }, + ActionsetPodSpecs: sp.JSONMap{ + "tolerations": []map[string]interface{}{ + { + "key": "taint-key", + "operator": "Equal", + "value": "taint-value", + "effect": "NoSchedule", + }, + }, + }, + Expected: v1.PodSpec{ + NodeSelector: map[string]string{ + "selector-key": "selector-value", + }, + Tolerations: []v1.Toleration{ + { + Key: "taint-key", + Operator: v1.TolerationOpEqual, + Value: "taint-value", + Effect: v1.TaintEffectNoSchedule, + }, + }, + Containers: []v1.Container{ + { + Name: "container", + Image: "kanisterio/kanister-tools:0.21.0", + Command: []string{"sh", "-c", "echo in default specs"}, + ImagePullPolicy: v1.PullPolicy(v1.PullAlways), + VolumeMounts: []v1.VolumeMount{ + { + Name: "data", + MountPath: "/var/lib/data", + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyOnFailure, + Volumes: []v1.Volume{ + { + Name: "data", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "default-pvc", + }, + }, + }, + }, + }, + }, + + // Add NodeSelector and Tolerations. Override container command + { + BlueprintPodSpecs: sp.JSONMap{ + "nodeSelector": map[string]interface{}{ + "selector-key": "selector-value", + }, + "tolerations": []map[string]interface{}{ + { + "key": "taint-key", + "operator": "Equal", + "value": "taint-value", + "effect": "NoSchedule", + }, + }, + }, + ActionsetPodSpecs: sp.JSONMap{ + "containers": []map[string]interface{}{ + { + "name": "container", + "command": []string{"echo", "override command"}, + }, + }, + }, + Expected: v1.PodSpec{ + NodeSelector: map[string]string{ + "selector-key": "selector-value", + }, + Tolerations: []v1.Toleration{ + { + Key: "taint-key", + Operator: v1.TolerationOpEqual, + Value: "taint-value", + Effect: v1.TaintEffectNoSchedule, + }, + }, + Containers: []v1.Container{ + { + Name: "container", + Image: "kanisterio/kanister-tools:0.21.0", + Command: []string{"echo", "override command"}, + ImagePullPolicy: v1.PullPolicy(v1.PullAlways), + VolumeMounts: []v1.VolumeMount{ + { + Name: "data", + MountPath: "/var/lib/data", + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyOnFailure, + Volumes: []v1.Volume{ + { + Name: "data", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "default-pvc", + }, + }, + }, + }, + }, + }, + + // Override container command + { + BlueprintPodSpecs: sp.JSONMap{ + "containers": []map[string]interface{}{ + { + "name": "container", + "command": []string{"echo", "override command"}, + }, + }, + }, + ActionsetPodSpecs: nil, + Expected: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "container", + Image: "kanisterio/kanister-tools:0.21.0", + Command: []string{"echo", "override command"}, + ImagePullPolicy: v1.PullPolicy(v1.PullAlways), + VolumeMounts: []v1.VolumeMount{ + { + Name: "data", + MountPath: "/var/lib/data", + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyOnFailure, + Volumes: []v1.Volume{ + { + Name: "data", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "default-pvc", + }, + }, + }, + }, + }, + }, + + // Override blueprint specs with actionset + { + BlueprintPodSpecs: sp.JSONMap{ + "containers": []map[string]interface{}{ + { + "name": "container", + "imagePullPolicy": "IfNotPresent", + }, + }, + "dnsPolicy": "Default", + }, + ActionsetPodSpecs: sp.JSONMap{ + "dnsPolicy": "ClusterFirst", + }, + Expected: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "container", + Image: "kanisterio/kanister-tools:0.21.0", + Command: []string{"sh", "-c", "echo in default specs"}, + ImagePullPolicy: v1.PullPolicy(v1.PullIfNotPresent), + VolumeMounts: []v1.VolumeMount{ + { + Name: "data", + MountPath: "/var/lib/data", + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyOnFailure, + Volumes: []v1.Volume{ + { + Name: "data", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "default-pvc", + }, + }, + }, + }, + DNSPolicy: v1.DNSClusterFirst, + }, + }, + } + + // Run tests + for _, test := range tests { + override, err := CreateAndMergeJsonPatch(test.BlueprintPodSpecs, test.ActionsetPodSpecs) + c.Assert(err, IsNil) + podSpec, err := patchDefaultPodSpecs(defaultSpecs, override) + c.Assert(err, IsNil) + c.Assert(podSpec, DeepEquals, test.Expected) + } + +} diff --git a/pkg/testing/e2e_test.go b/pkg/testing/e2e_test.go index 4eecd6e6ca..48f22db129 100644 --- a/pkg/testing/e2e_test.go +++ b/pkg/testing/e2e_test.go @@ -90,6 +90,93 @@ func (s *E2ESuite) TestKubeExec(c *C) { err = kube.WaitOnDeploymentReady(ctx, s.cli, s.namespace, d.GetName()) c.Assert(err, IsNil) + // Create a dummy Profile and secret + sec := testutil.NewTestProfileSecret() + sec, err = s.cli.CoreV1().Secrets(s.namespace).Create(sec) + c.Assert(err, IsNil) + + p := testutil.NewTestProfile(s.namespace, sec.GetName()) + p, err = s.crCli.Profiles(s.namespace).Create(p) + c.Assert(err, IsNil) + + // Create a simple Blueprint + bp := &crv1alpha1.Blueprint{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-blueprint-", + }, + Actions: map[string]*crv1alpha1.BlueprintAction{ + "test": &crv1alpha1.BlueprintAction{ + Kind: "Deployment", + Phases: []crv1alpha1.BlueprintPhase{ + crv1alpha1.BlueprintPhase{ + Func: "KubeExec", + Name: "test-kube-exec", + Args: map[string]interface{}{ + "namespace": "{{ .Deployment.Namespace }}", + "pod": "{{ index .Deployment.Pods 0 }}", + "container": "test-container", + "command": []string{"echo", "hello"}, + }, + }, + }, + }, + }, + } + bp, err = s.crCli.Blueprints(s.namespace).Create(bp) + c.Assert(err, IsNil) + + // Create an ActionSet + as := &crv1alpha1.ActionSet{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-actionset-", + }, + Spec: &crv1alpha1.ActionSetSpec{ + Actions: []crv1alpha1.ActionSpec{ + crv1alpha1.ActionSpec{ + Name: "test", + Object: crv1alpha1.ObjectReference{ + Kind: "Deployment", + Name: d.GetName(), + Namespace: s.namespace, + }, + Blueprint: bp.GetName(), + Profile: &crv1alpha1.ObjectReference{ + Name: p.GetName(), + Namespace: s.namespace, + }, + }, + }, + }, + } + as, err = s.crCli.ActionSets(s.namespace).Create(as) + c.Assert(err, IsNil) + + // Wait for the ActionSet to complete. + err = poll.Wait(ctx, func(ctx context.Context) (bool, error) { + as, err = s.crCli.ActionSets(s.namespace).Get(as.GetName(), metav1.GetOptions{}) + switch { + case err != nil, as.Status == nil: + return false, err + case as.Status.State == crv1alpha1.StateFailed: + return true, errors.Errorf("Actionset failed: %#v", as.Status) + case as.Status.State == crv1alpha1.StateComplete: + return true, nil + } + return false, nil + }) + c.Assert(err, IsNil) +} + +func (s *E2ESuite) TestKubeTask(c *C) { + ctx, can := context.WithTimeout(context.Background(), 30*time.Second) + defer can() + + // Create a test Deployment + d, err := s.cli.AppsV1().Deployments(s.namespace).Create(testutil.NewTestDeployment(1)) + c.Assert(err, IsNil) + err = kube.WaitOnDeploymentReady(ctx, s.cli, s.namespace, d.GetName()) + c.Assert(err, IsNil) + // Create a dummy Profile and secret sec := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -134,13 +221,21 @@ func (s *E2ESuite) TestKubeExec(c *C) { Kind: "Deployment", Phases: []crv1alpha1.BlueprintPhase{ crv1alpha1.BlueprintPhase{ - Func: "KubeExec", - Name: "test-kube-exec", + Func: "KubeTask", + Name: "test-kube-task", Args: map[string]interface{}{ + "image": "kanisterio/kanister-tools:0.21.0", "namespace": "{{ .Deployment.Namespace }}", - "pod": "{{ index .Deployment.Pods 0 }}", - "container": "test-container", - "command": []string{"echo", "hello"}, + "command": []string{"echo", "default specs"}, + "podOverride": map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "name": "container", + "imagePullPolicy": "IfNotPresent", + }, + }, + "dnsPolicy": "Default", + }, }, }, }, @@ -169,6 +264,9 @@ func (s *E2ESuite) TestKubeExec(c *C) { Name: p.GetName(), Namespace: s.namespace, }, + PodOverride: map[string]interface{}{ + "dnsPolicy": "ClusterFirst", + }, }, }, },