diff --git a/build.go b/build.go index 08f4403c21..1fe7880c29 100644 --- a/build.go +++ b/build.go @@ -35,6 +35,7 @@ import ( "github.com/buildpacks/pack/internal/stack" "github.com/buildpacks/pack/internal/stringset" "github.com/buildpacks/pack/internal/style" + "github.com/buildpacks/pack/internal/termui" "github.com/buildpacks/pack/logging" "github.com/buildpacks/pack/pkg/archive" projectTypes "github.com/buildpacks/pack/pkg/project/types" @@ -125,6 +126,9 @@ type BuildOptions struct { // Only trust builders from reputable sources. TrustBuilder bool + // Launch a terminal UI to depict the build process + Interactive bool + // List of buildpack images or archives to add to a builder. // These buildpacks may overwrite those on the builder if they // share both an ID and Version with a buildpack on the builder. @@ -329,6 +333,8 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { Workspace: opts.Workspace, GID: opts.GroupID, PreviousImage: opts.PreviousImage, + Interactive: opts.Interactive, + Termui: termui.NewTermui(), } lifecycleVersion := ephemeralBuilder.LifecycleDescriptor().Info.Version diff --git a/build_test.go b/build_test.go index 57ac02e8d7..08a53d02b7 100644 --- a/build_test.go +++ b/build_test.go @@ -2394,6 +2394,17 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, fakeLifecycle.Opts.PreviousImage, "example.com/some/new:tag") }) }) + + when("interactive option", func() { + it("passthroughs to lifecycle", func() { + h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ + Builder: defaultBuilderName, + Image: "example.com/some/repo:tag", + Interactive: true, + })) + h.AssertEq(t, fakeLifecycle.Opts.Interactive, true) + }) + }) }) } diff --git a/go.mod b/go.mod index bc5ccb2c48..611cf68688 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/buildpacks/lifecycle v0.11.4 github.com/docker/docker v20.10.8+incompatible github.com/docker/go-connections v0.4.0 + github.com/gdamore/tcell/v2 v2.3.3 github.com/ghodss/yaml v1.0.0 github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.6 @@ -22,6 +23,7 @@ require ( github.com/opencontainers/image-spec v1.0.1 github.com/pelletier/go-toml v1.9.3 github.com/pkg/errors v0.9.1 + github.com/rivo/tview v0.0.0-20210624165335-29d673af0ce2 github.com/sabhiram/go-gitignore v0.0.0-20201211074657-223ce5d391b0 github.com/sclevine/spec v1.4.0 github.com/sergi/go-diff v1.1.0 // indirect diff --git a/go.sum b/go.sum index af4da415c8..a234af11de 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0 github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab h1:9pygWVFqbY9lPxM0peffumuVDyMuIMzNLyO9uFjJuQo= +github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= @@ -72,6 +74,8 @@ github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXn github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= +github.com/Microsoft/hcsshim v0.8.10 h1:k5wTrpnVU2/xv8ZuzGkbXVd3js5zJ8RnumPo5RxiIxU= +github.com/Microsoft/hcsshim v0.8.10/go.mod h1:g5uw8EV2mAlzqe94tfNBNdr89fnbD/n3HV0OhsddkmM= github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= github.com/Microsoft/hcsshim v0.8.16 h1:8/auA4LFIZFTGrqfKhGBSXwM6/4X1fHa/xniyEHu8ac= @@ -171,6 +175,7 @@ github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1: github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY= github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= @@ -182,10 +187,11 @@ github.com/containerd/containerd v1.5.2/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTV github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41 h1:kIFnQBO7rQ0XkMe6xEwbybYHBEaWmh/f++laI6Emt7M= +github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY= github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= -github.com/containerd/continuity v0.1.0 h1:UFRRY5JemiAhPZrr/uE0n8fMTLcZsUvySPr1+D7pgr8= github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= @@ -207,6 +213,7 @@ github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJ github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/stargz-snapshotter/estargz v0.4.1 h1:5e7heayhB7CcgdTkqfZqrNaNv15gABwr3Q2jBTbLlt4= github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= github.com/containerd/stargz-snapshotter/estargz v0.7.0 h1:1d/rydzTywc76lnjJb6qbPCiTiCwts49AzKps/Ecblw= github.com/containerd/stargz-snapshotter/estargz v0.7.0/go.mod h1:83VWDqHnurTKliEB0YvWMiCfLDwv4Cjj1X9Vk98GJZw= @@ -267,6 +274,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v0.0.0-20200312141509-ef2f64abbd37 h1:MKHpi6ibJ9V5iuyUABEppUcvP0idDC1klY+UuiSFSPc= github.com/docker/cli v0.0.0-20200312141509-ef2f64abbd37/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v20.10.7+incompatible h1:pv/3NqibQKphWZiAskMzdz8w0PRbtTaEB+f6NwdU7Is= github.com/docker/cli v20.10.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= @@ -316,6 +324,10 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.3.3 h1:RKoI6OcqYrr/Do8yHZklecdGzDTJH9ACKdfECbRdw3M= +github.com/gdamore/tcell/v2 v2.3.3/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -410,6 +422,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-containerregistry v0.4.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= +github.com/google/go-containerregistry v0.5.1 h1:/+mFTs4AlwsJ/mJe8NDtKb7BxLtbZFpcn8vDsneEkwQ= +github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/go-containerregistry v0.6.0 h1:niQ+8XD//kKgArIFwDVBXsWVWbde16LPdHMyNwSC8h4= github.com/google/go-containerregistry v0.6.0/go.mod h1:euCCtNbZ6tKqi1E72vwDj2xZcN5ttKpZLfa/wSo5iLw= github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo= @@ -533,6 +547,9 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -552,6 +569,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -576,6 +595,7 @@ github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQ github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/sys/mount v0.2.0 h1:WhCW5B355jtxndN5ovugJlMFJawbUODuW8fSnEH6SSM= github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM= +github.com/moby/sys/mountinfo v0.4.0 h1:1KInV3Huv18akCu58V7lzNlt+jFmqlu1EaErnEHE/VM= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= @@ -633,6 +653,7 @@ github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= @@ -644,6 +665,7 @@ github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.m github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opencontainers/selinux v1.6.0 h1:+bIAS/Za3q5FTwWym4fTB0vObnfCf3G/NC7K6Jx62mY= github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= github.com/opencontainers/selinux v1.8.0 h1:+77ba4ar4jsCbL1GLbFL8fFM57w6suPfSS9PDLDY7KM= github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= @@ -693,6 +715,11 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rivo/tview v0.0.0-20210624165335-29d673af0ce2 h1:I5N0WNMgPSq5NKUFspB4jMJ6n2P0ipz5FlOlB4BXviQ= +github.com/rivo/tview v0.0.0-20210624165335-29d673af0ce2/go.mod h1:IxQujbYMAh4trWr0Dwa8jfciForjVmxyHpskZX6aydQ= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -847,6 +874,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= @@ -884,6 +912,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0 h1:UG21uOlmZabA4fW5i7ZX6bjw1xELEGg/ZLgZq9auk/Q= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= @@ -936,6 +965,7 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= @@ -951,6 +981,7 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= @@ -1043,12 +1074,14 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1056,6 +1089,9 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c h1:Lyn7+CqXIiC+LOR9aHD6jDK+hPcmAuCfuXztd1v4w1Q= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1071,8 +1107,8 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1310,8 +1346,8 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/build/container_ops.go b/internal/build/container_ops.go index 5c249e78a9..8ea6a093ac 100644 --- a/internal/build/container_ops.go +++ b/internal/build/container_ops.go @@ -120,12 +120,14 @@ func copyDirWindows(ctx context.Context, ctrClient client.CommonAPIClient, conta return errors.Wrap(err, "copy app to container") } - return container.Run( + return container.RunWithHandler( ctx, ctrClient, ctr.ID, - ioutil.Discard, // Suppress xcopy output - stderr, + container.DefaultHandler( + ioutil.Discard, // Suppress xcopy output + stderr, + ), ) } @@ -268,12 +270,14 @@ func EnsureVolumeAccess(uid, gid int, os string, volumeNames ...string) Containe } defer ctrClient.ContainerRemove(context.Background(), ctr.ID, types.ContainerRemoveOptions{Force: true}) - return container.Run( + return container.RunWithHandler( ctx, ctrClient, ctr.ID, - ioutil.Discard, // Suppress icacls output - stderr, + container.DefaultHandler( + ioutil.Discard, // Suppress icacls output + stderr, + ), ) } } diff --git a/internal/build/container_ops_test.go b/internal/build/container_ops_test.go index aa507b159e..0b3026cf4f 100644 --- a/internal/build/container_ops_test.go +++ b/internal/build/container_ops_test.go @@ -92,7 +92,7 @@ func testContainerOps(t *testing.T, when spec.G, it spec.S) { err = copyDirOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) + err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf)) h.AssertNil(t, err) h.AssertEq(t, errBuf.String(), "") @@ -147,7 +147,7 @@ lrwxrwxrwx 1 123 456 (.*) fake-app-symlink -> fake-app-file err = copyDirOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) + err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf)) h.AssertNil(t, err) h.AssertEq(t, errBuf.String(), "") @@ -196,7 +196,7 @@ drwsrwsrwt 2 123 456 (.*) some-vol err = copyDirOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) + err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf)) h.AssertNil(t, err) h.AssertEq(t, errBuf.String(), "") @@ -226,7 +226,7 @@ drwsrwsrwt 2 123 456 (.*) some-vol err = copyDirOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) + err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf)) h.AssertNil(t, err) h.AssertEq(t, errBuf.String(), "") @@ -276,7 +276,7 @@ drwsrwsrwt 2 123 456 (.*) some-vol err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) + err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf)) h.AssertNil(t, err) h.AssertEq(t, errBuf.String(), "") @@ -319,7 +319,7 @@ drwsrwsrwt 2 123 456 (.*) some-vol err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) + err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf)) h.AssertNil(t, err) h.AssertEq(t, errBuf.String(), "") @@ -364,7 +364,7 @@ drwsrwsrwt 2 123 456 (.*) some-vol err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) + err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf)) h.AssertNil(t, err) h.AssertEq(t, errBuf.String(), "") @@ -409,7 +409,7 @@ drwsrwsrwt 2 123 456 (.*) some-vol err = writeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) + err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf)) h.AssertEq(t, errBuf.String(), "") h.AssertNil(t, err) @@ -458,7 +458,7 @@ drwsrwsrwt 2 123 456 (.*) some-vol initVolumeOp := build.EnsureVolumeAccess(123, 456, osType, ctrVolumes[0], ctrVolumes[0]) err = initVolumeOp(ctrClient, ctx, ctr.ID, &outBuf, &errBuf) h.AssertNil(t, err) - err = container.Run(ctx, ctrClient, ctr.ID, &outBuf, &errBuf) + err = container.RunWithHandler(ctx, ctrClient, ctr.ID, container.DefaultHandler(&outBuf, &errBuf)) h.AssertNil(t, err) h.AssertEq(t, errBuf.String(), "") diff --git a/internal/build/fakes/fake_termui.go b/internal/build/fakes/fake_termui.go new file mode 100644 index 0000000000..a7d5395ebc --- /dev/null +++ b/internal/build/fakes/fake_termui.go @@ -0,0 +1,75 @@ +package fakes + +import ( + "io" + + "github.com/buildpacks/pack/internal/build" + "github.com/buildpacks/pack/internal/container" +) + +type FakeTermui struct { + handler container.Handler +} + +func NewFakeTermui(handler container.Handler) *FakeTermui { + return &FakeTermui{ + handler: handler, + } +} + +func (f *FakeTermui) Run(funk func()) error { + return nil +} + +func (f *FakeTermui) Handler() container.Handler { + return f.handler +} + +func WithTermui(screen build.Termui) func(*build.LifecycleOptions) { + return func(opts *build.LifecycleOptions) { + opts.Interactive = true + opts.Termui = screen + } +} + +func (f *FakeTermui) Debug(msg string) { + // not implemented +} + +func (f *FakeTermui) Debugf(fmt string, v ...interface{}) { + // not implemented +} + +func (f *FakeTermui) Info(msg string) { + // not implemented +} + +func (f *FakeTermui) Infof(fmt string, v ...interface{}) { + // not implemented +} + +func (f *FakeTermui) Warn(msg string) { + // not implemented +} + +func (f *FakeTermui) Warnf(fmt string, v ...interface{}) { + // not implemented +} + +func (f *FakeTermui) Error(msg string) { + // not implemented +} + +func (f *FakeTermui) Errorf(fmt string, v ...interface{}) { + // not implemented +} + +func (f *FakeTermui) Writer() io.Writer { + // not implemented + return nil +} + +func (f *FakeTermui) IsVerbose() bool { + // not implemented + return false +} diff --git a/internal/build/lifecycle_execution.go b/internal/build/lifecycle_execution.go index 01dcb39bbd..8f721d252f 100644 --- a/internal/build/lifecycle_execution.go +++ b/internal/build/lifecycle_execution.go @@ -61,6 +61,10 @@ func NewLifecycleExecution(logger logging.Logger, docker client.CommonAPIClient, mountPaths: mountPathsForOS(osType, opts.Workspace), } + if opts.Interactive { + exec.logger = opts.Termui + } + return exec, nil } diff --git a/internal/build/lifecycle_executor.go b/internal/build/lifecycle_executor.go index 9879801777..4efc84bfc8 100644 --- a/internal/build/lifecycle_executor.go +++ b/internal/build/lifecycle_executor.go @@ -5,8 +5,6 @@ import ( "math/rand" "time" - "github.com/buildpacks/pack/internal/cache" - "github.com/buildpacks/imgutil" "github.com/buildpacks/lifecycle/api" "github.com/buildpacks/lifecycle/platform" @@ -14,6 +12,8 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/buildpacks/pack/internal/builder" + "github.com/buildpacks/pack/internal/cache" + "github.com/buildpacks/pack/internal/container" "github.com/buildpacks/pack/logging" ) @@ -47,6 +47,13 @@ type Cache interface { Type() cache.Type } +type Termui interface { + logging.Logger + + Run(funk func()) error + Handler() container.Handler +} + func init() { rand.Seed(time.Now().UTC().UnixNano()) } @@ -63,6 +70,8 @@ type LifecycleOptions struct { Publish bool TrustBuilder bool UseCreator bool + Interactive bool + Termui Termui DockerHost string CacheImage string HTTPProxy string @@ -87,6 +96,14 @@ func (l *LifecycleExecutor) Execute(ctx context.Context, opts LifecycleOptions) if err != nil { return err } - defer lifecycleExec.Cleanup() - return lifecycleExec.Run(ctx, NewDefaultPhaseFactory) + + if !opts.Interactive { + defer lifecycleExec.Cleanup() + return lifecycleExec.Run(ctx, NewDefaultPhaseFactory) + } + + return opts.Termui.Run(func() { + defer lifecycleExec.Cleanup() + lifecycleExec.Run(ctx, NewDefaultPhaseFactory) + }) } diff --git a/internal/build/phase.go b/internal/build/phase.go index 5850417880..3793977d06 100644 --- a/internal/build/phase.go +++ b/internal/build/phase.go @@ -17,6 +17,7 @@ type Phase struct { infoWriter io.Writer errorWriter io.Writer docker client.CommonAPIClient + handler container.Handler ctrConf *dcontainer.Config hostConf *dcontainer.HostConfig ctr dcontainer.ContainerCreateCreatedBody @@ -39,12 +40,16 @@ func (p *Phase) Run(ctx context.Context) error { } } - return container.Run( + handler := container.DefaultHandler(p.infoWriter, p.errorWriter) + if p.handler != nil { + handler = p.handler + } + + return container.RunWithHandler( ctx, p.docker, p.ctr.ID, - p.infoWriter, - p.errorWriter, + handler, ) } diff --git a/internal/build/phase_config_provider.go b/internal/build/phase_config_provider.go index d428f12bf0..3b375fa28d 100644 --- a/internal/build/phase_config_provider.go +++ b/internal/build/phase_config_provider.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/api/types/container" + pcontainer "github.com/buildpacks/pack/internal/container" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/logging" ) @@ -28,6 +29,7 @@ type PhaseConfigProvider struct { containerOps []ContainerOperation infoWriter io.Writer errorWriter io.Writer + handler pcontainer.Handler } func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops ...PhaseConfigProviderOperation) *PhaseConfigProvider { @@ -73,6 +75,11 @@ func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops lifecycleExec.logger.Debug("Host Settings:") lifecycleExec.logger.Debugf(" Binds: %s", style.Symbol(strings.Join(provider.hostConf.Binds, " "))) lifecycleExec.logger.Debugf(" Network Mode: %s", style.Symbol(string(provider.hostConf.NetworkMode))) + + if lifecycleExec.opts.Interactive { + provider.handler = lifecycleExec.opts.Termui.Handler() + } + return provider } @@ -88,6 +95,10 @@ func (p *PhaseConfigProvider) HostConfig() *container.HostConfig { return p.hostConf } +func (p *PhaseConfigProvider) Handler() pcontainer.Handler { + return p.handler +} + func (p *PhaseConfigProvider) Name() string { return p.name } diff --git a/internal/build/phase_config_provider_test.go b/internal/build/phase_config_provider_test.go index 2dbfec0a24..405ceedb4a 100644 --- a/internal/build/phase_config_provider_test.go +++ b/internal/build/phase_config_provider_test.go @@ -2,6 +2,7 @@ package build_test import ( "bytes" + "io" "math/rand" "testing" "time" @@ -11,6 +12,7 @@ import ( "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/client" "github.com/heroku/color" + "github.com/pkg/errors" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -78,6 +80,20 @@ func testPhaseConfigProvider(t *testing.T, when spec.G, it spec.S) { }) }) + when("building with interactive mode", func() { + it("returns a phase config provider with interactive args", func() { + handler := func(bodyChan <-chan container.ContainerWaitOKBody, errChan <-chan error, reader io.Reader) error { + return errors.New("i was called") + } + + fakeTermui := fakes.NewFakeTermui(handler) + lifecycle := newTestLifecycleExec(t, false, fakes.WithTermui(fakeTermui)) + phaseConfigProvider := build.NewPhaseConfigProvider("some-name", lifecycle) + + h.AssertError(t, phaseConfigProvider.Handler()(nil, nil, nil), "i was called") + }) + }) + when("called with WithArgs", func() { it("sets args on the config", func() { lifecycle := newTestLifecycleExec(t, false) diff --git a/internal/build/phase_factory.go b/internal/build/phase_factory.go index 04525cfc2b..ff6248b6c6 100644 --- a/internal/build/phase_factory.go +++ b/internal/build/phase_factory.go @@ -29,6 +29,7 @@ func (m *DefaultPhaseFactory) New(provider *PhaseConfigProvider) RunnerCleaner { docker: m.lifecycleExec.docker, infoWriter: provider.InfoWriter(), errorWriter: provider.ErrorWriter(), + handler: provider.handler, uid: m.lifecycleExec.opts.Builder.UID(), gid: m.lifecycleExec.opts.Builder.GID(), appPath: m.lifecycleExec.opts.AppPath, diff --git a/internal/build/phase_test.go b/internal/build/phase_test.go index 3c00d8b192..326fcd428f 100644 --- a/internal/build/phase_test.go +++ b/internal/build/phase_test.go @@ -19,6 +19,7 @@ import ( "github.com/buildpacks/imgutil/local" "github.com/buildpacks/lifecycle/auth" + dcontainer "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/authn" @@ -28,6 +29,7 @@ import ( "github.com/buildpacks/pack/internal/build" "github.com/buildpacks/pack/internal/build/fakes" + "github.com/buildpacks/pack/internal/container" ilogging "github.com/buildpacks/pack/internal/logging" "github.com/buildpacks/pack/logging" "github.com/buildpacks/pack/pkg/archive" @@ -138,6 +140,25 @@ func testPhase(t *testing.T, when spec.G, it spec.S) { h.AssertContains(t, outBuf.String(), "file contents: test-app") }) + it("runs the phase with provided handlers", func() { + var actual string + var handler container.Handler = func(bodyChan <-chan dcontainer.ContainerWaitOKBody, errChan <-chan error, reader io.Reader) error { + data, _ := ioutil.ReadAll(reader) + actual = string(data) + return nil + } + + var err error + lifecycleExec, err = CreateFakeLifecycleExecution(logger, docker, filepath.Join("testdata", "fake-app"), repoName, handler) + h.AssertNil(t, err) + phaseFactory = build.NewDefaultPhaseFactory(lifecycleExec) + + configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec) + phase := phaseFactory.New(configProvider) + assertRunSucceeds(t, phase, nil, nil) + h.AssertContains(t, actual, "running some-lifecycle-phase") + }) + it("copies the app into the app volume", func() { configProvider := build.NewPhaseConfigProvider( phaseName, @@ -457,7 +478,7 @@ func assertRunSucceeds(t *testing.T, phase build.RunnerCleaner, outBuf *bytes.Bu h.AssertNilE(t, phase.Cleanup()) } -func CreateFakeLifecycleExecution(logger logging.Logger, docker client.CommonAPIClient, appDir string, repoName string) (*build.LifecycleExecution, error) { +func CreateFakeLifecycleExecution(logger logging.Logger, docker client.CommonAPIClient, appDir string, repoName string, handler ...container.Handler) (*build.LifecycleExecution, error) { builderImage, err := local.NewImage(repoName, docker, local.FromBaseImage(repoName)) if err != nil { return nil, err @@ -471,12 +492,24 @@ func CreateFakeLifecycleExecution(logger logging.Logger, docker client.CommonAPI return nil, err } + var ( + interactive bool + termui build.Termui + ) + + if len(handler) != 0 { + interactive = true + termui = fakes.NewFakeTermui(handler[0]) + } + return build.NewLifecycleExecution(logger, docker, build.LifecycleOptions{ - AppPath: appDir, - Builder: fakeBuilder, - HTTPProxy: "some-http-proxy", - HTTPSProxy: "some-https-proxy", - NoProxy: "some-no-proxy", + AppPath: appDir, + Builder: fakeBuilder, + HTTPProxy: "some-http-proxy", + HTTPSProxy: "some-https-proxy", + NoProxy: "some-no-proxy", + Interactive: interactive, + Termui: termui, }) } diff --git a/internal/commands/build.go b/internal/commands/build.go index 0d1e9284b8..f5eb668e4a 100644 --- a/internal/commands/build.go +++ b/internal/commands/build.go @@ -26,6 +26,7 @@ type BuildFlags struct { Publish bool ClearCache bool TrustBuilder bool + Interactive bool DockerHost string CacheImage string AppPath string @@ -162,6 +163,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob LifecycleImage: lifecycleImage, GroupID: gid, PreviousImage: flags.PreviousImage, + Interactive: flags.Interactive, }); err != nil { return errors.Wrap(err, "failed to build") } @@ -202,6 +204,10 @@ This option may set DOCKER_HOST environment variable for the build container if cmd.Flags().StringVar(&buildFlags.Workspace, "workspace", "", "Location at which to mount the app dir in the build image") cmd.Flags().IntVar(&buildFlags.GID, "gid", 0, `Override GID of user's group in the stack's build and run images. The provided value must be a positive number`) cmd.Flags().StringVar(&buildFlags.PreviousImage, "previous-image", "", "Set previous image to a particular tag reference, digest reference, or (when performing a daemon build) image ID") + cmd.Flags().BoolVar(&buildFlags.Interactive, "interactive", false, "Launch a terminal UI to depict the build process") + if !cfg.Experimental { + cmd.Flags().MarkHidden("interactive") + } } func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackClient, logger logging.Logger) error { @@ -216,6 +222,11 @@ func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackCli if flags.GID < 0 { return errors.New("gid flag must be in the range of 0-2147483647") } + + if flags.Interactive && !cfg.Experimental { + return pack.NewExperimentError("Interactive mode is currently experimental.") + } + return nil } diff --git a/internal/commands/build_test.go b/internal/commands/build_test.go index 779d8d7dcc..b5a6d311c3 100644 --- a/internal/commands/build_test.go +++ b/internal/commands/build_test.go @@ -780,6 +780,15 @@ builder = "my-builder" }) }) }) + + when("interactive flag is provided but experimental isn't set in the config", func() { + it("errors with a descriptive message", func() { + command.SetArgs([]string{"image", "--interactive"}) + err := command.Execute() + h.AssertNotNil(t, err) + h.AssertError(t, err, "Interactive mode is currently experimental.") + }) + }) }) } diff --git a/internal/container/run.go b/internal/container/run.go index 3fe096a750..e39f8fbb23 100644 --- a/internal/container/run.go +++ b/internal/container/run.go @@ -12,7 +12,9 @@ import ( "github.com/pkg/errors" ) -func Run(ctx context.Context, docker client.CommonAPIClient, ctrID string, out, errOut io.Writer) error { +type Handler func(bodyChan <-chan dcontainer.ContainerWaitOKBody, errChan <-chan error, reader io.Reader) error + +func RunWithHandler(ctx context.Context, docker client.CommonAPIClient, ctrID string, handler Handler) error { bodyChan, errChan := docker.ContainerWait(ctx, ctrID, dcontainer.WaitConditionNextExit) resp, err := docker.ContainerAttach(ctx, ctrID, types.ContainerAttachOptions{ @@ -29,25 +31,31 @@ func Run(ctx context.Context, docker client.CommonAPIClient, ctrID string, out, return errors.Wrap(err, "container start") } - copyErr := make(chan error) - go func() { - _, err := stdcopy.StdCopy(out, errOut, resp.Reader) - defer optionallyCloseWriter(out) - defer optionallyCloseWriter(errOut) + return handler(bodyChan, errChan, resp.Reader) +} + +func DefaultHandler(out, errOut io.Writer) Handler { + return func(bodyChan <-chan dcontainer.ContainerWaitOKBody, errChan <-chan error, reader io.Reader) error { + copyErr := make(chan error) + go func() { + _, err := stdcopy.StdCopy(out, errOut, reader) + defer optionallyCloseWriter(out) + defer optionallyCloseWriter(errOut) - copyErr <- err - }() + copyErr <- err + }() - select { - case body := <-bodyChan: - if body.StatusCode != 0 { - return fmt.Errorf("failed with status code: %d", body.StatusCode) + select { + case body := <-bodyChan: + if body.StatusCode != 0 { + return fmt.Errorf("failed with status code: %d", body.StatusCode) + } + case err := <-errChan: + return err } - case err := <-errChan: - return err - } - return <-copyErr + return <-copyErr + } } func optionallyCloseWriter(writer io.Writer) error { diff --git a/internal/termui/detect.go b/internal/termui/detect.go new file mode 100644 index 0000000000..ad7836b28b --- /dev/null +++ b/internal/termui/detect.go @@ -0,0 +1,82 @@ +package termui + +import ( + "time" + + "github.com/rivo/tview" +) + +type Detect struct { + app app + textView *tview.TextView + doneChan chan bool +} + +func NewDetect(app app) *Detect { + d := &Detect{ + app: app, + textView: detectStatusTV(app), + doneChan: make(chan bool, 1), + } + + go d.start() + grid := centered(d.textView) + d.app.SetRoot(grid, true) + return d +} + +func (d *Detect) Stop() { + d.doneChan <- true +} + +func (d *Detect) start() { + var ( + i = 0 + ticker = time.NewTicker(250 * time.Millisecond) + doneText = "⌛️ Detected!" + texts = []string{ + "⏳️ Detecting", + "⏳️ Detecting.", + "⏳️ Detecting..", + "⏳️ Detecting...", + } + ) + + for { + select { + case <-ticker.C: + d.textView.SetText(texts[i]) + + i++ + if i == len(texts) { + i = 0 + } + case <-d.doneChan: + ticker.Stop() + d.textView.SetText(doneText) + return + } + } +} + +func detectStatusTV(app app) *tview.TextView { + tv := tview.NewTextView() + tv.SetBackgroundColor(backgroundColor) + tv.SetChangedFunc(func() { app.Draw() }) + return tv +} + +func centered(p tview.Primitive) tview.Primitive { + return tview.NewGrid(). + SetColumns(0, 20, 0). + SetRows(0, 1, 0). + AddItem(tview.NewBox().SetBackgroundColor(backgroundColor), 0, 0, 1, 1, 0, 0, true). + AddItem(tview.NewBox().SetBackgroundColor(backgroundColor), 0, 1, 1, 1, 0, 0, true). + AddItem(tview.NewBox().SetBackgroundColor(backgroundColor), 0, 2, 1, 1, 0, 0, true). + AddItem(tview.NewBox().SetBackgroundColor(backgroundColor), 1, 0, 1, 1, 0, 0, true). + AddItem(p, 1, 1, 1, 1, 0, 0, true). + AddItem(tview.NewBox().SetBackgroundColor(backgroundColor), 1, 2, 1, 1, 0, 0, true). + AddItem(tview.NewBox().SetBackgroundColor(backgroundColor), 2, 0, 1, 1, 0, 0, true). + AddItem(tview.NewBox().SetBackgroundColor(backgroundColor), 2, 1, 1, 1, 0, 0, true). + AddItem(tview.NewBox().SetBackgroundColor(backgroundColor), 2, 2, 1, 1, 0, 0, true) +} diff --git a/internal/termui/fakes/app.go b/internal/termui/fakes/app.go new file mode 100644 index 0000000000..ca43f05687 --- /dev/null +++ b/internal/termui/fakes/app.go @@ -0,0 +1,37 @@ +package fakes + +import ( + "github.com/rivo/tview" +) + +type App struct { + SetRootCallCount int + DrawCallCount int + + doneChan chan bool +} + +func NewApp() *App { + return &App{ + doneChan: make(chan bool, 1), + } +} + +func (a *App) SetRoot(root tview.Primitive, fullscreen bool) *tview.Application { + a.SetRootCallCount++ + return nil +} + +func (a *App) Draw() *tview.Application { + a.DrawCallCount++ + return nil +} + +func (a *App) Run() error { + <-a.doneChan + return nil +} + +func (a *App) StopRunning() { + a.doneChan <- true +} diff --git a/internal/termui/fakes/docker_stdwriter.go b/internal/termui/fakes/docker_stdwriter.go new file mode 100644 index 0000000000..d3629278dd --- /dev/null +++ b/internal/termui/fakes/docker_stdwriter.go @@ -0,0 +1,40 @@ +package fakes + +import ( + "io" + "time" + + "github.com/docker/docker/pkg/stdcopy" +) + +type DockerStdWriter struct { + wOut io.Writer + wErr io.Writer +} + +func NewDockerStdWriter(w io.Writer) *DockerStdWriter { + return &DockerStdWriter{ + wOut: stdcopy.NewStdWriter(w, stdcopy.Stdout), + wErr: stdcopy.NewStdWriter(w, stdcopy.Stderr), + } +} + +func (w *DockerStdWriter) WriteStdoutln(contents string) { + w.write(contents+"\n", stdcopy.Stdout) +} + +func (w *DockerStdWriter) WriteStderrln(contents string) { + w.write(contents+"\n", stdcopy.Stderr) +} + +func (w *DockerStdWriter) write(contents string, t stdcopy.StdType) { + switch t { + case stdcopy.Stdout: + w.wOut.Write([]byte(contents)) + case stdcopy.Stderr: + w.wErr.Write([]byte(contents)) + } + + // guard against race conditions + time.Sleep(time.Millisecond) +} diff --git a/internal/termui/logger.go b/internal/termui/logger.go new file mode 100644 index 0000000000..0f07791da3 --- /dev/null +++ b/internal/termui/logger.go @@ -0,0 +1,45 @@ +package termui + +import "io" + +func (s *Termui) Debug(msg string) { + // not implemented +} + +func (s *Termui) Debugf(fmt string, v ...interface{}) { + // not implemented +} + +func (s *Termui) Info(msg string) { + s.textChan <- msg +} + +func (s *Termui) Infof(fmt string, v ...interface{}) { + // not implemented +} + +func (s *Termui) Warn(msg string) { + // not implemented +} + +func (s *Termui) Warnf(fmt string, v ...interface{}) { + // not implemented +} + +func (s *Termui) Error(msg string) { + // not implemented +} + +func (s *Termui) Errorf(fmt string, v ...interface{}) { + // not implemented +} + +func (s *Termui) Writer() io.Writer { + // not implemented + return nil +} + +func (s *Termui) IsVerbose() bool { + // not implemented + return false +} diff --git a/internal/termui/termui.go b/internal/termui/termui.go new file mode 100644 index 0000000000..08609789e0 --- /dev/null +++ b/internal/termui/termui.go @@ -0,0 +1,110 @@ +package termui + +import ( + "bufio" + "io" + "io/ioutil" + "strings" + + dcontainer "github.com/docker/docker/api/types/container" + "github.com/docker/docker/pkg/stdcopy" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + + "github.com/buildpacks/pack/internal/container" +) + +var ( + backgroundColor = tcell.NewRGBColor(5, 30, 40) +) + +type app interface { + SetRoot(root tview.Primitive, fullscreen bool) *tview.Application + Draw() *tview.Application + Run() error +} + +type page interface { + Stop() +} + +type Termui struct { + app app + currentPage page + textChan chan string +} + +func NewTermui() *Termui { + return &Termui{ + app: tview.NewApplication(), + textChan: make(chan string, 10), + } +} + +// Run starts the terminal UI process in the foreground +// and the passed in function in the background +func (s *Termui) Run(funk func()) error { + go funk() + go s.handle() + defer s.stop() + + s.currentPage = NewDetect(s.app) + return s.app.Run() +} + +func (s *Termui) stop() { + close(s.textChan) +} + +func (s *Termui) handle() { + for txt := range s.textChan { + switch { + case strings.Contains(txt, "===> ANALYZING"): + s.currentPage.Stop() + default: + // no-op + } + } +} + +func (s *Termui) Handler() container.Handler { + return func(bodyChan <-chan dcontainer.ContainerWaitOKBody, errChan <-chan error, reader io.Reader) error { + var ( + copyErr = make(chan error) + r, w = io.Pipe() + scanner = bufio.NewScanner(r) + ) + + go func() { + defer w.Close() + + _, err := stdcopy.StdCopy(w, ioutil.Discard, reader) + if err != nil { + copyErr <- err + } + }() + + for { + select { + //TODO: errors should show up on screen + // instead of halting loop + //See: https://github.com/buildpacks/pack/issues/1262 + case err := <-copyErr: + return err + case err := <-errChan: + return err + default: + if !scanner.Scan() { + err := scanner.Err() + if err != nil { + return err + } + + return nil + } + + s.textChan <- scanner.Text() + } + } + } +} diff --git a/internal/termui/termui_test.go b/internal/termui/termui_test.go new file mode 100644 index 0000000000..743e6b45ab --- /dev/null +++ b/internal/termui/termui_test.go @@ -0,0 +1,108 @@ +package termui + +import ( + "bytes" + "errors" + "fmt" + "io" + "strings" + "testing" + "time" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/pack/internal/termui/fakes" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestScreen(t *testing.T) { + spec.Run(t, "Termui", testTermui, spec.Report(report.Terminal{})) +} + +func testTermui(t *testing.T, when spec.G, it spec.S) { + var ( + assert = h.NewAssertionManager(t) + eventuallyInterval = 500 * time.Millisecond + eventuallyDuration = 5 * time.Second + ) + + it("performs the lifecycle", func() { + var ( + fakeApp = fakes.NewApp() + s = Termui{app: fakeApp, textChan: make(chan string, 10)} + r, w = io.Pipe() + fakeDockerStdWriter = fakes.NewDockerStdWriter(w) + ) + + defer func() { + w.Close() + fakeApp.StopRunning() + }() + go s.Run(func() {}) + go s.Handler()(nil, nil, r) + + h.Eventually(t, func() bool { + return fakeApp.SetRootCallCount == 1 + }, eventuallyInterval, eventuallyDuration) + + currentPage, ok := s.currentPage.(*Detect) + assert.TrueWithMessage(ok, fmt.Sprintf("expected %T to be assignable to type `*screen.Detect`", s.currentPage)) + assert.TrueWithMessage(fakeApp.DrawCallCount > 0, "expect app.Draw() to be called") + h.Eventually(t, func() bool { + return strings.Contains(currentPage.textView.GetText(false), "Detecting") + }, eventuallyInterval, eventuallyDuration) + + fakeDockerStdWriter.WriteStdoutln(`===> ANALYZING`) + h.Eventually(t, func() bool { + return strings.Contains(currentPage.textView.GetText(false), "Detected!") + }, eventuallyInterval, eventuallyDuration) + }) + + it("performs the lifecycle (when the builder is untrusted)", func() { + var ( + fakeApp = fakes.NewApp() + s = Termui{app: fakeApp, textChan: make(chan string, 10)} + r, w = io.Pipe() + ) + + defer func() { + w.Close() + fakeApp.StopRunning() + }() + go s.Run(func() {}) + go s.Handler()(nil, nil, r) + + h.Eventually(t, func() bool { + return fakeApp.SetRootCallCount == 1 + }, eventuallyInterval, eventuallyDuration) + + assert.Equal(fakeApp.SetRootCallCount, 1) + currentPage, ok := s.currentPage.(*Detect) + assert.TrueWithMessage(ok, fmt.Sprintf("expected %T to be assignable to type `*screen.Detect`", s.currentPage)) + assert.TrueWithMessage(fakeApp.DrawCallCount > 0, "expect app.Draw() to be called") + h.Eventually(t, func() bool { + return strings.Contains(currentPage.textView.GetText(false), "Detecting") + }, eventuallyInterval, eventuallyDuration) + + s.Info(`===> ANALYZING`) + h.Eventually(t, func() bool { + return strings.Contains(currentPage.textView.GetText(false), "Detected!") + }, eventuallyInterval, eventuallyDuration) + }) + + // TODO: change to show errors on-screen + // See: https://github.com/buildpacks/pack/issues/1262 + it("returns errors from error channel", func() { + var ( + errChan = make(chan error, 1) + fakeApp = fakes.NewApp() + s = Termui{app: fakeApp} + ) + + errChan <- errors.New("some-error") + + err := s.Handler()(nil, errChan, bytes.NewReader(nil)) + assert.ErrorContains(err, "some-error") + }) +}