github.com/pachyderm/pachyderm@v1.13.4/src/server/pps/cmds/cmds_test.go (about)

     1  // TODO(msteffen) Add tests for:
     2  //
     3  // - restart datum
     4  // - stop job
     5  // - delete job
     6  //
     7  // - inspect job
     8  // - list job
     9  //
    10  // - create pipeline
    11  // - create pipeline --push-images (re-enable existing test)
    12  // - update pipeline
    13  // - delete pipeline
    14  //
    15  // - inspect pipeline
    16  // - list pipeline
    17  //
    18  // - start pipeline
    19  // - stop pipeline
    20  //
    21  // - list datum
    22  // - inspect datum
    23  // - logs
    24  
    25  package cmds
    26  
    27  import (
    28  	"bytes"
    29  	"fmt"
    30  	"os"
    31  	"testing"
    32  
    33  	"github.com/pachyderm/pachyderm/src/client/pkg/require"
    34  	tu "github.com/pachyderm/pachyderm/src/server/pkg/testutil"
    35  )
    36  
    37  const badJSON1 = `
    38  {
    39  "356weryt
    40  
    41  }
    42  `
    43  
    44  const badJSON2 = `{
    45  {
    46      "a": 1,
    47      "b": [23,4,4,64,56,36,7456,7],
    48      "c": {"e,f,g,h,j,j},
    49      "d": 3452.36456,
    50  }
    51  `
    52  
    53  func TestSyntaxErrorsReportedCreatePipeline(t *testing.T) {
    54  	if testing.Short() {
    55  		t.Skip("Skipping integration tests in short mode")
    56  	}
    57  	require.NoError(t, tu.BashCmd(`
    58  		echo -n '{{.badJSON1}}' \
    59  		  | ( pachctl create pipeline -f - 2>&1 || true ) \
    60  		  | match "malformed pipeline spec"
    61  
    62  		echo -n '{{.badJSON2}}' \
    63  		  | ( pachctl create pipeline -f - 2>&1 || true ) \
    64  		  | match "malformed pipeline spec"
    65  		`,
    66  		"badJSON1", badJSON1,
    67  		"badJSON2", badJSON2,
    68  	).Run())
    69  }
    70  
    71  func TestSyntaxErrorsReportedUpdatePipeline(t *testing.T) {
    72  	if testing.Short() {
    73  		t.Skip("Skipping integration tests in short mode")
    74  	}
    75  	require.NoError(t, tu.BashCmd(`
    76  		echo -n '{{.badJSON1}}' \
    77  		  | ( pachctl update pipeline -f - 2>&1 || true ) \
    78  		  | match "malformed pipeline spec"
    79  
    80  		echo -n '{{.badJSON2}}' \
    81  		  | ( pachctl update pipeline -f - 2>&1 || true ) \
    82  		  | match "malformed pipeline spec"
    83  		`,
    84  		"badJSON1", badJSON1,
    85  		"badJSON2", badJSON2,
    86  	).Run())
    87  }
    88  
    89  func TestRawFullPipelineInfo(t *testing.T) {
    90  	if testing.Short() {
    91  		t.Skip("Skipping integration tests in short mode")
    92  	}
    93  	require.NoError(t, tu.BashCmd(`
    94  		yes | pachctl delete all
    95  		pachctl garbage-collect
    96  	`).Run())
    97  	require.NoError(t, tu.BashCmd(`
    98  		pachctl create repo data
    99  		pachctl put file data@master:/file <<<"This is a test"
   100  		pachctl create pipeline <<EOF
   101  		  {
   102  		    "pipeline": {"name": "{{.pipeline}}"},
   103  		    "input": {
   104  		      "pfs": {
   105  		        "glob": "/*",
   106  		        "repo": "data"
   107  		      }
   108  		    },
   109  		    "transform": {
   110  		      "cmd": ["bash"],
   111  		      "stdin": ["cp /pfs/data/file /pfs/out"]
   112  		    }
   113  		  }
   114  		EOF
   115  		`,
   116  		"pipeline", tu.UniqueString("p-")).Run())
   117  	require.NoError(t, tu.BashCmd(`
   118  		pachctl flush commit data@master
   119  
   120  		# make sure the results have the full pipeline info, including version
   121  		pachctl list job --raw --history=all \
   122  			| match "pipeline_version"
   123  		`).Run())
   124  }
   125  
   126  // TestJSONMultiplePipelines tests that pipeline specs with multiple pipelines
   127  // in them continue to be accepted by 'pachctl create pipeline'. We may want to
   128  // stop supporting this behavior eventually, but Pachyderm has supported it
   129  // historically, so we should continue to support it until we formally deprecate
   130  // it.
   131  func TestJSONMultiplePipelines(t *testing.T) {
   132  	if testing.Short() {
   133  		t.Skip("Skipping integration tests in short mode")
   134  	}
   135  	require.NoError(t, tu.BashCmd(`
   136  		yes | pachctl delete all
   137  		pachctl create repo input
   138  		pachctl create pipeline -f - <<EOF
   139  		{
   140  		  "pipeline": {
   141  		    "name": "first"
   142  		  },
   143  		  "input": {
   144  		    "pfs": {
   145  		      "glob": "/*",
   146  		      "repo": "input"
   147  		    }
   148  		  },
   149  		  "transform": {
   150  		    "cmd": [ "/bin/bash" ],
   151  		    "stdin": [
   152  		      "cp /pfs/input/* /pfs/out"
   153  		    ]
   154  		  }
   155  		}
   156  		{
   157  		  "pipeline": {
   158  		    "name": "second"
   159  		  },
   160  		  "input": {
   161  		    "pfs": {
   162  		      "glob": "/*",
   163  		      "repo": "first"
   164  		    }
   165  		  },
   166  		  "transform": {
   167  		    "cmd": [ "/bin/bash" ],
   168  		    "stdin": [
   169  		      "cp /pfs/first/* /pfs/out"
   170  		    ]
   171  		  }
   172  		}
   173  		EOF
   174  
   175  		pachctl start commit input@master
   176  		echo foo | pachctl put file input@master:/foo
   177  		echo bar | pachctl put file input@master:/bar
   178  		echo baz | pachctl put file input@master:/baz
   179  		pachctl finish commit input@master
   180  		pachctl flush commit input@master
   181  		pachctl get file second@master:/foo | match foo
   182  		pachctl get file second@master:/bar | match bar
   183  		pachctl get file second@master:/baz | match baz
   184  		`,
   185  	).Run())
   186  	require.NoError(t, tu.BashCmd(`pachctl list pipeline`).Run())
   187  }
   188  
   189  // TestJSONStringifiedNumberstests that JSON pipelines may use strings to
   190  // specify numeric values such as a pipeline's parallelism (a feature of gogo's
   191  // JSON parser).
   192  func TestJSONStringifiedNumbers(t *testing.T) {
   193  	if testing.Short() {
   194  		t.Skip("Skipping integration tests in short mode")
   195  	}
   196  	require.NoError(t, tu.BashCmd(`
   197  		yes | pachctl delete all
   198  		pachctl create repo input
   199  		pachctl create pipeline -f - <<EOF
   200  		{
   201  		  "pipeline": {
   202  		    "name": "first"
   203  		  },
   204  		  "input": {
   205  		    "pfs": {
   206  		      "glob": "/*",
   207  		      "repo": "input"
   208  		    }
   209  		  },
   210  		  "parallelism_spec": {
   211  		    "constant": "1"
   212  		  },
   213  		  "transform": {
   214  		    "cmd": [ "/bin/bash" ],
   215  		    "stdin": [
   216  		      "cp /pfs/input/* /pfs/out"
   217  		    ]
   218  		  }
   219  		}
   220  		EOF
   221  
   222  		pachctl start commit input@master
   223  		echo foo | pachctl put file input@master:/foo
   224  		echo bar | pachctl put file input@master:/bar
   225  		echo baz | pachctl put file input@master:/baz
   226  		pachctl finish commit input@master
   227  		pachctl flush commit input@master
   228  		pachctl get file first@master:/foo | match foo
   229  		pachctl get file first@master:/bar | match bar
   230  		pachctl get file first@master:/baz | match baz
   231  		`,
   232  	).Run())
   233  	require.NoError(t, tu.BashCmd(`pachctl list pipeline`).Run())
   234  }
   235  
   236  // TestJSONMultiplePipelinesError tests that when creating multiple pipelines
   237  // (which only the encoding/json parser can parse) you get an error indicating
   238  // the problem in the JSON, rather than an error complaining about multiple
   239  // documents.
   240  func TestJSONMultiplePipelinesError(t *testing.T) {
   241  	if testing.Short() {
   242  		t.Skip("Skipping integration tests in short mode")
   243  	}
   244  	// pipeline spec has no quotes around "name" in first pipeline
   245  	require.NoError(t, tu.BashCmd(`
   246  		yes | pachctl delete all
   247  		pachctl create repo input
   248  		( pachctl create pipeline -f - 2>&1 <<EOF || true
   249  		{
   250  		  "pipeline": {
   251  		    name: "first"
   252  		  },
   253  		  "input": {
   254  		    "pfs": {
   255  		      "glob": "/*",
   256  		      "repo": "input"
   257  		    }
   258  		  },
   259  		  "transform": {
   260  		    "cmd": [ "/bin/bash" ],
   261  		    "stdin": [
   262  		      "cp /pfs/input/* /pfs/out"
   263  		    ]
   264  		  }
   265  		}
   266  		{
   267  		  "pipeline": {
   268  		    "name": "second"
   269  		  },
   270  		  "input": {
   271  		    "pfs": {
   272  		      "glob": "/*",
   273  		      "repo": "first"
   274  		    }
   275  		  },
   276  		  "transform": {
   277  		    "cmd": [ "/bin/bash" ],
   278  		    "stdin": [
   279  		      "cp /pfs/first/* /pfs/out"
   280  		    ]
   281  		  }
   282  		}
   283  		EOF
   284  		) | match "invalid character 'n' looking for beginning of object key string"
   285  		`,
   286  	).Run())
   287  }
   288  
   289  func TestRunPipeline(t *testing.T) {
   290  	if os.Getenv("RUN_BAD_TESTS") == "" {
   291  		t.Skip("Skipping because RUN_BAD_TESTS was empty")
   292  	}
   293  	if testing.Short() {
   294  		t.Skip("Skipping integration tests in short mode")
   295  	}
   296  	pipeline := tu.UniqueString("p-")
   297  	require.NoError(t, tu.BashCmd(`
   298  		yes | pachctl delete all
   299  		pachctl garbage-collect
   300  	`).Run())
   301  	require.NoError(t, tu.BashCmd(`
   302  		pachctl create repo data
   303  
   304  		# Create a commit and put some data in it
   305  		commit1=$(pachctl start commit data@master)
   306  		echo "file contents" | pachctl put file data@${commit1}:/file -f -
   307  		pachctl finish commit data@${commit1}
   308  
   309  		pachctl put file data@master:/file <<<"This is a test"
   310  		pachctl create pipeline <<EOF
   311  			{
   312  			  "pipeline": {"name": "{{.pipeline}}"},
   313  			  "input": {
   314  			    "pfs": {
   315  			      "glob": "/*",
   316  			      "repo": "data"
   317  			    }
   318  			  },
   319  			  "transform": {
   320  			    "cmd": ["bash"],
   321  			    "stdin": ["cp /pfs/data/file /pfs/out"]
   322  			  }
   323  			}
   324  		EOF
   325  
   326  		# make sure the run pipeline command accepts various provenance formats
   327  		pachctl run pipeline {{.pipeline}} data@${commit1}
   328  		pachctl run pipeline {{.pipeline}} data@master=${commit1}
   329  		pachctl run pipeline {{.pipeline}} data@master
   330  		`,
   331  		"pipeline", pipeline).Run())
   332  }
   333  
   334  // TestYAMLPipelineSpec tests creating a pipeline with a YAML pipeline spec
   335  func TestYAMLPipelineSpec(t *testing.T) {
   336  	if os.Getenv("RUN_BAD_TESTS") == "" {
   337  		t.Skip("Skipping because RUN_BAD_TESTS was empty")
   338  	}
   339  
   340  	if testing.Short() {
   341  		t.Skip("Skipping integration tests in short mode")
   342  	}
   343  	// Note that BashCmd dedents all lines below including the YAML (which
   344  	// wouldn't parse otherwise)
   345  	require.NoError(t, tu.BashCmd(`
   346  		yes | pachctl delete all
   347  		pachctl create repo input
   348  		pachctl create pipeline -f - <<EOF
   349  		pipeline:
   350  		  name: first
   351  		input:
   352  		  pfs:
   353  		    glob: /*
   354  		    repo: input
   355  		transform:
   356  		  cmd: [ /bin/bash ]
   357  		  stdin:
   358  		    - "cp /pfs/input/* /pfs/out"
   359  		---
   360  		pipeline:
   361  		  name: second
   362  		input:
   363  		  pfs:
   364  		    glob: /*
   365  		    repo: first
   366  		transform:
   367  		  cmd: [ /bin/bash ]
   368  		  stdin:
   369  		    - "cp /pfs/first/* /pfs/out"
   370  		EOF
   371  		pachctl start commit input@master
   372  		echo foo | pachctl put file input@master:/foo
   373  		echo bar | pachctl put file input@master:/bar
   374  		echo baz | pachctl put file input@master:/baz
   375  		pachctl finish commit input@master
   376  		pachctl flush commit input@master
   377  		pachctl get file second@master:/foo | match foo
   378  		pachctl get file second@master:/bar | match bar
   379  		pachctl get file second@master:/baz | match baz
   380  		`,
   381  	).Run())
   382  }
   383  
   384  func TestListPipelineFilter(t *testing.T) {
   385  	if os.Getenv("RUN_BAD_TESTS") == "" {
   386  		t.Skip("Skipping because RUN_BAD_TESTS was empty")
   387  	}
   388  
   389  	if testing.Short() {
   390  		t.Skip("Skipping integration tests in short mode")
   391  	}
   392  	require.NoError(t, tu.BashCmd(`
   393  		yes | pachctl delete all
   394  		pachctl garbage-collect
   395  	`).Run())
   396  	require.NoError(t, tu.BashCmd(`
   397  		yes | pachctl delete all
   398  		pachctl create repo input
   399  		pachctl create pipeline -f - <<EOF
   400  		{
   401  		"pipeline": {
   402  		  "name": "first"
   403  		},
   404  		"input": {
   405  		  "pfs": {
   406  		    "glob": "/*",
   407  		    "repo": "input"
   408  		  }
   409  		},
   410  		  "parallelism_spec": {
   411  		    "constant": "1"
   412  		  },
   413  		"transform": {
   414  		  "cmd": [ "/bin/bash" ],
   415  		  "stdin": [
   416  		    "cp /pfs/input/* /pfs/out"
   417  		  ]
   418  		}
   419  		}
   420  		EOF
   421  
   422  		echo foo | pachctl put file input@master:/foo
   423  		pachctl flush commit input@master
   424  		# make sure we see the pipeline with the appropriate state filters
   425  		pachctl list pipeline | match first
   426  		pachctl list pipeline --state starting --state running | match first
   427  		pachctl list pipeline --state crashing --state failure | match -v first
   428  	`,
   429  	).Run())
   430  }
   431  
   432  // TestYAMLError tests that when creating pipelines using a YAML spec with an
   433  // error, you get an error indicating the problem in the YAML, rather than an
   434  // error complaining about multiple documents.
   435  //
   436  // Note that with the new parsing method added to support free-form fields like
   437  // TFJob, this YAML is parsed, serialized and then re-parsed, so the error will
   438  // refer to "json" (the format used for the canonicalized pipeline), but the
   439  // issue referenced by the error (use of a string instead of an array for 'cmd')
   440  // is the main problem below
   441  func TestYAMLError(t *testing.T) {
   442  	if testing.Short() {
   443  		t.Skip("Skipping integration tests in short mode")
   444  	}
   445  	// "cmd" should be a list, instead of a string
   446  	require.NoError(t, tu.BashCmd(`
   447  		yes | pachctl delete all
   448  		pachctl create repo input
   449  		( pachctl create pipeline -f - 2>&1 <<EOF || true
   450  		pipeline:
   451  		  name: first
   452  		input:
   453  		  pfs:
   454  		    glob: /*
   455  		    repo: input
   456  		transform:
   457  		  cmd: /bin/bash # should be list, instead of string
   458  		  stdin:
   459  		    - "cp /pfs/input/* /pfs/out"
   460  		EOF
   461  		) | match "cannot unmarshal string into Go value of type \[\]json.RawMessage"
   462  		`,
   463  	).Run())
   464  }
   465  
   466  func TestTFJobBasic(t *testing.T) {
   467  	if testing.Short() {
   468  		t.Skip("Skipping integration tests in short mode")
   469  	}
   470  	require.NoError(t, tu.BashCmd(`
   471  		yes | pachctl delete all
   472  		pachctl create repo input
   473  		( pachctl create pipeline -f - 2>&1 <<EOF || true
   474  		pipeline:
   475  		  name: first
   476  		input:
   477  		  pfs:
   478  		    glob: /*
   479  		    repo: input
   480  		tf_job:
   481  		  apiVersion: kubeflow.org/v1
   482  		  kind: TFJob
   483  		  metadata:
   484  		    generateName: tfjob
   485  		    namespace: kubeflow
   486  		  spec:
   487  		    tfReplicaSpecs:
   488  		      PS:
   489  		        replicas: 1
   490  		        restartPolicy: OnFailure
   491  		        template:
   492  		          spec:
   493  		            containers:
   494  		            - name: tensorflow
   495  		              image: gcr.io/your-project/your-image
   496  		              command:
   497  		                - python
   498  		                - -m
   499  		                - trainer.task
   500  		                - --batch_size=32
   501  		                - --training_steps=1000
   502  		      Worker:
   503  		        replicas: 3
   504  		        restartPolicy: OnFailure
   505  		        template:
   506  		          spec:
   507  		            containers:
   508  		            - name: tensorflow
   509  		              image: gcr.io/your-project/your-image
   510  		              command:
   511  		                - python
   512  		                - -m
   513  		                - trainer.task
   514  		                - --batch_size=32
   515  		                - --training_steps=1000
   516  		EOF
   517  		) | match "not supported yet"
   518  		`,
   519  	).Run())
   520  }
   521  
   522  // TestYAMLSecret tests creating a YAML pipeline with a secret (i.e. the fix for
   523  // https://github.com/pachyderm/pachyderm/issues/4119)
   524  func TestYAMLSecret(t *testing.T) {
   525  	if testing.Short() {
   526  		t.Skip("Skipping integration tests in short mode")
   527  	}
   528  	// Note that BashCmd dedents all lines below including the YAML (which
   529  	// wouldn't parse otherwise)
   530  	require.NoError(t, tu.BashCmd(`
   531  		yes | pachctl delete all
   532  
   533  		# kubectl get secrets >&2
   534  		kubectl delete secrets/test-yaml-secret || true
   535  		kubectl create secret generic test-yaml-secret --from-literal=my-key=my-value
   536  
   537  		pachctl create repo input
   538  		pachctl put file input@master:/foo <<<"foo"
   539  		pachctl create pipeline -f - <<EOF
   540  		  pipeline:
   541  		    name: pipeline
   542  		  input:
   543  		    pfs:
   544  		      glob: /*
   545  		      repo: input
   546  		  transform:
   547  		    cmd: [ /bin/bash ]
   548  		    stdin:
   549  		      - "env | grep MY_SECRET >/pfs/out/vars"
   550  		    secrets:
   551  		      - name: test-yaml-secret
   552  		        env_var: MY_SECRET
   553  		        key: my-key
   554  		EOF
   555  		pachctl flush commit input@master
   556  		pachctl get file pipeline@master:/vars | match MY_SECRET=my-value
   557  		`,
   558  	).Run())
   559  }
   560  
   561  // TestYAMLTimestamp tests creating a YAML pipeline with a timestamp (i.e. the
   562  // fix for https://github.com/pachyderm/pachyderm/issues/4209)
   563  func TestYAMLTimestamp(t *testing.T) {
   564  	if testing.Short() {
   565  		t.Skip("Skipping integration tests in short mode")
   566  	}
   567  	// Note that BashCmd dedents all lines below including the YAML (which
   568  	// wouldn't parse otherwise)
   569  	require.NoError(t, tu.BashCmd(`
   570  		yes | pachctl delete all
   571  
   572  		# If the pipeline comes up without error, then the YAML parsed
   573  		pachctl create pipeline -f - <<EOF
   574  		  pipeline:
   575  		    name: pipeline
   576  		  input:
   577  		    cron:
   578  		      name: in
   579  		      start: "2019-10-10T22:30:05Z"
   580  		      spec: "@yearly"
   581  		  transform:
   582  		    cmd: [ /bin/bash ]
   583  		    stdin:
   584  		      - "cp /pfs/in/* /pfs/out"
   585  		EOF
   586  		pachctl list pipeline | match 'pipeline'
   587  		`,
   588  	).Run())
   589  }
   590  
   591  func TestEditPipeline(t *testing.T) {
   592  	if testing.Short() {
   593  		t.Skip("Skipping integration tests in short mode")
   594  	}
   595  	require.NoError(t, tu.BashCmd(`
   596  		yes | pachctl delete all
   597  	`).Run())
   598  	require.NoError(t, tu.BashCmd(`
   599  		pachctl create repo data
   600  		pachctl create pipeline <<EOF
   601  		  pipeline:
   602  		    name: my-pipeline
   603  		  input:
   604  		    pfs:
   605  		      glob: /*
   606  		      repo: data
   607  		  transform:
   608  		    cmd: [ /bin/bash ]
   609  		    stdin:
   610  		      - "cp /pfs/data/* /pfs/out"
   611  		EOF
   612  		`).Run())
   613  	require.NoError(t, tu.BashCmd(`
   614  		EDITOR="cat -u" pachctl edit pipeline my-pipeline -o yaml \
   615  		| match 'name: my-pipeline' \
   616  		| match 'repo: data' \
   617  		| match 'cmd:' \
   618  		| match 'cp /pfs/data/\* /pfs/out'
   619  		`).Run())
   620  }
   621  
   622  func TestPipelineBuildLifecyclePython(t *testing.T) {
   623  	require.NoError(t, tu.BashCmd("yes | pachctl delete all").Run())
   624  	pipeline := testPipelineBuildLifecycle(t, "python", "python")
   625  
   626  	// the python example also contains a `.pachignore`, so we can verify it's
   627  	// intended behavior here
   628  	require.NoError(t, tu.BashCmd(`
   629  		pachctl get file {{.pipeline}}_build@source:/.pachignore
   630  	`, "pipeline", pipeline).Run())
   631  	require.YesError(t, tu.BashCmd(`
   632  		pachctl get file {{.pipeline}}_build@source:/foo.txt
   633  	`, "pipeline", pipeline).Run())
   634  }
   635  
   636  func TestPipelineBuildLifecyclePythonNoDeps(t *testing.T) {
   637  	require.NoError(t, tu.BashCmd("yes | pachctl delete all").Run())
   638  	testPipelineBuildLifecycle(t, "python", "python_no_deps")
   639  }
   640  
   641  func TestPipelineBuildLifecycleGo(t *testing.T) {
   642  	require.NoError(t, tu.BashCmd("yes | pachctl delete all").Run())
   643  	testPipelineBuildLifecycle(t, "go", "go")
   644  }
   645  
   646  func TestAuthorizedPipelineBuildLifecycle(t *testing.T) {
   647  	require.NoError(t, tu.BashCmd("yes | pachctl delete all").Run())
   648  	_ = tu.GetAuthenticatedPachClient(t, "unused") // enable auth as a side effect
   649  
   650  	defer tu.DeleteAll(t) // make sure to clean up auth
   651  
   652  	testPipelineBuildLifecycle(t, "go", "go")
   653  }
   654  
   655  func testPipelineBuildLifecycle(t *testing.T, lang, dir string) string {
   656  	t.Helper()
   657  	if testing.Short() {
   658  		t.Skip("Skipping integration tests in short mode")
   659  	}
   660  
   661  	prefix := "../../../../etc/testing/pipeline-build"
   662  
   663  	// reset and create some test input
   664  	require.NoError(t, tu.BashCmd(`
   665  		pachctl create repo in
   666  		pachctl put file -r in@master:/ -f {{.prefix}}/input
   667  	`, "prefix", prefix).Run())
   668  
   669  	// give pipeline a unique name to work around
   670  	// github.com/kubernetes/kubernetes/issues/82130
   671  	pipeline := tu.UniqueString("test-pipeline-build")
   672  	spec := fmt.Sprintf(`
   673  		{
   674  		  "pipeline": {
   675  		    "name": %q
   676  		  },
   677  		  "transform": {
   678  		    "build": {
   679  		      "language": %q
   680  		    }
   681  		  },
   682  		  "input": {
   683  		    "pfs": {
   684  		      "repo": "in",
   685  		      "glob": "/*"
   686  		    }
   687  		  }
   688  		}
   689  	`, pipeline, lang)
   690  
   691  	// test a barebones pipeline with a build spec and verify results
   692  	require.NoError(t, tu.BashCmd(`
   693  		cd {{.prefix}}/{{.dir}}
   694  		pachctl create pipeline <<EOF
   695  		{{.spec}}
   696  		EOF
   697  		pachctl flush commit in@master
   698  		`,
   699  		"dir", dir,
   700  		"spec", spec,
   701  		"prefix", prefix,
   702  	).Run())
   703  	require.YesError(t, tu.BashCmd(fmt.Sprintf(`
   704  		pachctl list pipeline --state failure | match %s
   705  	`, pipeline)).Run())
   706  	verifyPipelineBuildOutput(t, pipeline, "0")
   707  
   708  	// update the barebones pipeline and verify results
   709  	require.NoError(t, tu.BashCmd(`
   710  		cd {{.prefix}}/{{.dir}}
   711  		pachctl update pipeline <<EOF
   712  		{{.spec}}
   713  		EOF
   714  		pachctl flush commit in@master
   715  		`,
   716  		"dir", dir,
   717  		"spec", spec,
   718  		"prefix", prefix,
   719  	).Run())
   720  	verifyPipelineBuildOutput(t, pipeline, "0")
   721  
   722  	// update the pipeline with a custom cmd and verify results
   723  	spec = fmt.Sprintf(`
   724  		{
   725  		  "pipeline": {
   726  		    "name": %q
   727  		  },
   728  		  "transform": {
   729  		    "cmd": [
   730  		      "sh",
   731  		      "/pfs/build/run.sh",
   732  		      "_"
   733  		    ],
   734  		    "build": {
   735  		      "language": %q,
   736  		      "path": "."
   737  		    }
   738  		  },
   739  		  "input": {
   740  		    "pfs": {
   741  		      "repo": "in",
   742  		      "glob": "/*"
   743  		    }
   744  		  }
   745  		}
   746  	`, pipeline, lang)
   747  
   748  	require.NoError(t, tu.BashCmd(`
   749  		cd {{.prefix}}/{{.dir}}
   750  		pachctl update pipeline --reprocess <<EOF
   751  		{{.spec}}
   752  		EOF
   753  		pachctl flush commit in@master
   754  		`,
   755  		"dir", dir,
   756  		"spec", spec,
   757  		"prefix", prefix,
   758  	).Run())
   759  	verifyPipelineBuildOutput(t, pipeline, "_")
   760  	return pipeline
   761  }
   762  
   763  func verifyPipelineBuildOutput(t *testing.T, pipeline, prefix string) {
   764  	t.Helper()
   765  
   766  	require.NoError(t, tu.BashCmd(`
   767  		pachctl flush commit in@master
   768  		pachctl get file {{.pipeline}}@master:/1.txt | match {{.prefix}}{{.prefix}}{{.prefix}}1
   769  		pachctl get file {{.pipeline}}@master:/11.txt | match {{.prefix}}{{.prefix}}11
   770  		pachctl get file {{.pipeline}}@master:/111.txt | match {{.prefix}}111
   771  		`,
   772  		"pipeline", pipeline,
   773  		"prefix", prefix,
   774  	).Run())
   775  }
   776  
   777  func TestMissingPipeline(t *testing.T) {
   778  	if testing.Short() {
   779  		t.Skip("Skipping integration tests in short mode")
   780  	}
   781  	// should fail because there's no pipeline object in the spec
   782  	require.YesError(t, tu.BashCmd(`
   783  		pachctl create pipeline <<EOF
   784  		  {
   785  		    "transform": {
   786  		      "image": "ubuntu"
   787  		    },
   788  		    "input": {
   789  		      "pfs": {
   790  		        "repo": "in",
   791  		        "glob": "/*"
   792  		      }
   793  		    }
   794  		  }
   795  		EOF
   796  	`).Run())
   797  }
   798  
   799  func TestUnnamedPipeline(t *testing.T) {
   800  	if testing.Short() {
   801  		t.Skip("Skipping integration tests in short mode")
   802  	}
   803  	// should fail because there's no pipeline name
   804  	require.YesError(t, tu.BashCmd(`
   805  		pachctl create pipeline <<EOF
   806  		  {
   807  		    "pipeline": {},
   808  		    "transform": {
   809  		      "image": "ubuntu"
   810  		    },
   811  		    "input": {
   812  		      "pfs": {
   813  		        "repo": "in",
   814  		        "glob": "/*"
   815  		      }
   816  		    }
   817  		  }
   818  		EOF
   819  	`).Run())
   820  }
   821  
   822  func TestPipelineBuildSpout(t *testing.T) {
   823  	if testing.Short() {
   824  		t.Skip("Skipping integration tests in short mode")
   825  	}
   826  	// should fail because pipeline build can't be used w/ spouts
   827  	require.YesError(t, tu.BashCmd(`
   828  		pachctl create pipeline <<EOF
   829  		  {
   830  		    "pipeline": {
   831  		      "name": "test"
   832  		    },
   833  		    "transform": {
   834  		      "build": {
   835  		        "language": "go"
   836  		      }
   837  		    },
   838  		    "spout": {}
   839  		  }
   840  		EOF
   841  	`).Run())
   842  }
   843  
   844  func TestPipelineBuildMissingInput(t *testing.T) {
   845  	if testing.Short() {
   846  		t.Skip("Skipping integration tests in short mode")
   847  	}
   848  	// should fail because pipeline build can't be used w/ spouts
   849  	require.YesError(t, tu.BashCmd(`
   850  		pachctl create pipeline <<EOF
   851  		  {
   852  		    "pipeline": {
   853  		      "name": "test"
   854  		    },
   855  		    "transform": {
   856  		      "build": {
   857  		        "language": "go"
   858  		      }
   859  		    }
   860  		  }
   861  		EOF
   862  	`).Run())
   863  }
   864  
   865  func TestPipelineBuildBadInput(t *testing.T) {
   866  	if testing.Short() {
   867  		t.Skip("Skipping integration tests in short mode")
   868  	}
   869  	// should fail because 'build' and 'source' are reserved input names when
   870  	// using pipeline builds
   871  	require.YesError(t, tu.BashCmd(`
   872  		pachctl create pipeline <<EOF
   873  		  {
   874  		    "pipeline": {
   875  		      "name": "test"
   876  		    },
   877  		    "transform": {
   878  		      "build": {
   879  		        "language": "go"
   880  		      }
   881  		    },
   882  		    "input": {
   883  		      "pfs": {
   884  		        "name": "build",
   885  		        "repo": "in",
   886  		        "glob": "/*"
   887  		      }
   888  		    }
   889  		  }
   890  		EOF
   891  	`).Run())
   892  	require.YesError(t, tu.BashCmd(`
   893  		pachctl create pipeline <<EOF
   894  		  {
   895  		    "pipeline": {
   896  		      "name": "test"
   897  		    },
   898  		    "transform": {
   899  		      "build": {
   900  		        "language": "go"
   901  		      }
   902  		    },
   903  		    "input": {
   904  		      "pfs": {
   905  		        "name": "source",
   906  		        "repo": "in",
   907  		        "glob": "/*"
   908  		      }
   909  		    }
   910  		  }
   911  		EOF
   912  	`).Run())
   913  }
   914  
   915  func TestPipelineBuildMissingPath(t *testing.T) {
   916  	if testing.Short() {
   917  		t.Skip("Skipping integration tests in short mode")
   918  	}
   919  	// should fail because 'build' and 'source' are reserved input names for
   920  	// when using pipeline builds
   921  	require.YesError(t, tu.BashCmd(`
   922  		pachctl create pipeline <<EOF
   923  			{
   924  			  "pipeline": {
   925  			    "name": "test"
   926  			  },
   927  			  "transform": {
   928  			    "build": {
   929  			      "language": "go",
   930  			      "path": "/some/path/that/doesnt/exist"
   931  			    }
   932  			  },
   933  			  "input": {
   934  			    "pfs": {
   935  			      "repo": "in",
   936  			      "glob": "/*"
   937  			    }
   938  			  }
   939  			}
   940  		EOF
   941  	`).Run())
   942  }
   943  
   944  // func TestPushImages(t *testing.T) {
   945  // 	if testing.Short() {
   946  // 		t.Skip("Skipping integration tests in short mode")
   947  // 	}
   948  // 	ioutil.WriteFile("test-push-images.json", []byte(`{
   949  //   "pipeline": {
   950  //     "name": "test_push_images"
   951  //   },
   952  //   "transform": {
   953  //     "cmd": [ "true" ],
   954  // 	"image": "test-job-shim"
   955  //   }
   956  // }`), 0644)
   957  // 	os.Args = []string{"pachctl", "create", "pipeline", "--push-images", "-f", "test-push-images.json"}
   958  // 	require.NoError(t, rootCmd().Execute())
   959  // }
   960  
   961  func runPipelineWithImageGetStderr(t *testing.T, image string) (string, error) {
   962  	// reset and create some test input
   963  	require.NoError(t, tu.BashCmd(`
   964  		yes | pachctl delete all
   965  		pachctl create repo in
   966  		pachctl put file -r in@master:/ -f ../../../../etc/testing/pipeline-build/input
   967  	`).Run())
   968  
   969  	cmd := tu.BashCmd(`
   970  		pachctl create pipeline <<EOF
   971  		  {
   972  		    "pipeline": {
   973  		      "name": "first"
   974  		    },
   975  		    "transform": {
   976  		      "image": "{{.image}}"
   977  		    },
   978  		    "input": {
   979  		      "pfs": {
   980  		        "repo": "in",
   981  		        "glob": "/*"
   982  		      }
   983  		    }
   984  		  }
   985  		EOF
   986  	`, "image", image)
   987  	buf := &bytes.Buffer{}
   988  	cmd.Stderr = buf
   989  	// cmd.Stdout = os.Stdout // uncomment for debugging
   990  	cmd.Env = os.Environ()
   991  	err := cmd.Run()
   992  	stderr := buf.String()
   993  	return stderr, err
   994  }
   995  
   996  func TestPipelineBuildRunCron(t *testing.T) {
   997  	if testing.Short() {
   998  		t.Skip("Skipping integration tests in short mode")
   999  	}
  1000  
  1001  	// build a cron pipeline successfully
  1002  	require.NoError(t, tu.BashCmd(`
  1003  		pachctl create pipeline <<EOF
  1004  		{
  1005  		  "pipeline": {
  1006  		    "name": "crontest"
  1007  		  },
  1008  		  "input": {
  1009  		    "cron": {
  1010  		      "name": "tick",
  1011  		      "spec": "*/1 * * * *",
  1012  		      "overwrite": true
  1013  		    }
  1014  		  },
  1015  		  "transform": {
  1016  		    "build":{
  1017  		      "language": "go"
  1018  		    },
  1019  		    "cmd":["echo", "tick"]
  1020  		  },
  1021  		  "enable_stats": true
  1022  		  }
  1023  		EOF
  1024  	`).Run())
  1025  
  1026  	// and make sure you can use `run cron` on it
  1027  	require.NoError(t, tu.BashCmd(`
  1028  		pachctl run cron crontest
  1029  	`).Run())
  1030  }
  1031  
  1032  func TestWarningLatestTag(t *testing.T) {
  1033  	if testing.Short() {
  1034  		t.Skip("Skipping integration tests in short mode")
  1035  	}
  1036  	// should emit a warning because user specified latest tag on docker image
  1037  	stderr, err := runPipelineWithImageGetStderr(t, "ubuntu:latest")
  1038  	require.NoError(t, err, "%v", err)
  1039  	require.Matches(t, "WARNING", stderr)
  1040  }
  1041  
  1042  func TestWarningEmptyTag(t *testing.T) {
  1043  	if testing.Short() {
  1044  		t.Skip("Skipping integration tests in short mode")
  1045  	}
  1046  	// should emit a warning because user specified empty tag, equivalent to
  1047  	// :latest
  1048  	stderr, err := runPipelineWithImageGetStderr(t, "ubuntu")
  1049  	require.NoError(t, err)
  1050  	require.Matches(t, "WARNING", stderr)
  1051  }
  1052  
  1053  func TestNoWarningTagSpecified(t *testing.T) {
  1054  	if testing.Short() {
  1055  		t.Skip("Skipping integration tests in short mode")
  1056  	}
  1057  	// should not emit a warning (stderr should be empty) because user
  1058  	// specified non-empty, non-latest tag
  1059  	stderr, err := runPipelineWithImageGetStderr(t, "ubuntu:xenial")
  1060  	require.NoError(t, err)
  1061  	require.Equal(t, "", stderr)
  1062  }