├── .agignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── NOTICE.md ├── README.md ├── api ├── api_suite_test.go ├── handler.go ├── p2p_server.go ├── p2p_server_test.go ├── query.go ├── query_test.go ├── volume_server.go ├── volume_server_linux_test.go └── volume_server_test.go ├── baggageclaimcmd ├── command.go ├── driver_linux.go └── driver_nonlinux.go ├── baggageclaimfakes ├── fake_client.go ├── fake_volume.go └── fake_volume_future.go ├── ci ├── build-image.yml ├── pipeline.yml ├── prs-pipeline.yml ├── scripts │ ├── unit-darwin │ ├── unit-linux │ └── unit-windows.bat ├── unit-darwin.yml ├── unit-linux.yml └── unit-windows.yml ├── client.go ├── client ├── client.go ├── client_resiliency_test.go ├── client_suite_test.go ├── volume.go └── volume_future.go ├── client_test.go ├── cmd ├── baggageclaim │ └── main.go └── fs_mounter │ └── main.go ├── doc.go ├── errors.go ├── fs └── btrfs.go ├── go.mod ├── go.sum ├── integration ├── baggageclaim │ ├── copy_on_write_strategy_test.go │ ├── destroy_test.go │ ├── empty_strategy_test.go │ ├── import_strategy_test.go │ ├── integration.go │ ├── overlay_mounts_test.go │ ├── privileges_test.go │ ├── property_test.go │ ├── restart_test.go │ ├── startup_test.go │ └── suite_test.go └── fs_mounter │ ├── fs_mounter_test.go │ └── suite_test.go ├── kernel ├── LICENSE ├── NOTICE ├── kernel.go ├── kernel_unix.go ├── uname_linux.go ├── uname_solaris.go └── uname_unsupported.go ├── resources.go ├── routes.go ├── scripts └── test ├── suite_test.go ├── uidgid ├── mapper_linux.go ├── mapper_nonlinux.go ├── max_valid_uid.go ├── namespace.go ├── translator.go ├── uidgid.go ├── uidgid_linux.go └── uidgidfakes │ ├── fake_namespacer.go │ └── fake_translator.go └── volume ├── copy ├── copy_unix.go └── copy_windows.go ├── cow_strategy.go ├── cow_strategy_test.go ├── driver.go ├── driver ├── btrfs.go ├── btrfs_test.go ├── driver_suite_test.go ├── is_subvolume_linux.go ├── is_subvolume_stub.go ├── naive.go ├── overlay_linux.go └── overlay_linux_test.go ├── empty_strategy.go ├── empty_strategy_test.go ├── filesystem.go ├── import_strategy.go ├── locker.go ├── locker_test.go ├── metadata.go ├── promise.go ├── promise_list.go ├── promise_list_test.go ├── promise_test.go ├── properties.go ├── properties_test.go ├── repository.go ├── repository_test.go ├── strategerizer.go ├── strategerizer_test.go ├── strategy.go ├── stream.go ├── stream_in_out_linux.go ├── stream_in_out_nonlinux.go ├── volume.go ├── volume_suite_test.go └── volumefakes ├── fake_driver.go ├── fake_filesystem.go ├── fake_filesystem_init_volume.go ├── fake_filesystem_live_volume.go ├── fake_filesystem_volume.go ├── fake_lock_manager.go ├── fake_repository.go ├── fake_strategy.go └── fake_streamer.go /.agignore: -------------------------------------------------------------------------------- 1 | fake_*.go 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | tags 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1 2 | 3 | RUN apt-get update -qq && \ 4 | apt-get install -yqq \ 5 | file \ 6 | btrfs-progs 7 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | Copyright 2015-2017 Alex Suraci, Chris Brown, and Pivotal Software, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # baggage claim 2 | 3 | *a volume manager for garden containers* 4 | 5 | ![Baggage Claim](https://farm4.staticflickr.com/3365/4623535134_c88f474f8d_d.jpg) 6 | 7 | [by](https://creativecommons.org/licenses/by-nc-nd/2.0/) [atmx](https://www.flickr.com/photos/atmtx/) 8 | 9 | ## reporting issues and requesting features 10 | 11 | please report all issues and feature requests in [concourse/concourse](https://github.com/concourse/concourse/issues) 12 | 13 | ## about 14 | 15 | *baggageclaim* allows you to create and layer volumes on a remote server. This 16 | is particularly useful when used with [bind mounts][bind-mounts] or the RootFS 17 | when using [Garden][garden]. It allows directories to be made which can be 18 | populated before having copy-on-write layers layered on top. e.g. to provide 19 | caching. 20 | 21 | [bind-mounts]: http://man7.org/linux/man-pages/man8/mount.8.html#COMMAND-LINE_OPTIONS 22 | [garden]: https://github.com/cloudfoundry-incubator/garden-linux 23 | -------------------------------------------------------------------------------- /api/api_suite_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestAPI(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "API Suite") 13 | } 14 | -------------------------------------------------------------------------------- /api/handler.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "regexp" 7 | 8 | "code.cloudfoundry.org/lager" 9 | "github.com/tedsuo/rata" 10 | 11 | "github.com/concourse/baggageclaim" 12 | "github.com/concourse/baggageclaim/volume" 13 | ) 14 | 15 | func NewHandler( 16 | logger lager.Logger, 17 | strategerizer volume.Strategerizer, 18 | volumeRepo volume.Repository, 19 | p2pInterfacePattern *regexp.Regexp, 20 | p2pInterfaceFamily int, 21 | p2pStreamPort uint16, 22 | ) (http.Handler, error) { 23 | volumeServer := NewVolumeServer( 24 | logger.Session("volume-server"), 25 | strategerizer, 26 | volumeRepo, 27 | ) 28 | 29 | p2pServer := NewP2pServer( 30 | logger.Session("p2p-server"), 31 | p2pInterfacePattern, 32 | p2pInterfaceFamily, 33 | p2pStreamPort, 34 | ) 35 | 36 | handlers := rata.Handlers{ 37 | baggageclaim.CreateVolume: http.HandlerFunc(volumeServer.CreateVolume), 38 | baggageclaim.CreateVolumeAsync: http.HandlerFunc(volumeServer.CreateVolumeAsync), 39 | baggageclaim.CreateVolumeAsyncCancel: http.HandlerFunc(volumeServer.CreateVolumeAsyncCancel), 40 | baggageclaim.CreateVolumeAsyncCheck: http.HandlerFunc(volumeServer.CreateVolumeAsyncCheck), 41 | baggageclaim.ListVolumes: http.HandlerFunc(volumeServer.ListVolumes), 42 | baggageclaim.GetVolume: http.HandlerFunc(volumeServer.GetVolume), 43 | baggageclaim.SetProperty: http.HandlerFunc(volumeServer.SetProperty), 44 | baggageclaim.GetPrivileged: http.HandlerFunc(volumeServer.GetPrivileged), 45 | baggageclaim.SetPrivileged: http.HandlerFunc(volumeServer.SetPrivileged), 46 | baggageclaim.StreamIn: http.HandlerFunc(volumeServer.StreamIn), 47 | baggageclaim.StreamOut: http.HandlerFunc(volumeServer.StreamOut), 48 | baggageclaim.StreamP2pOut: http.HandlerFunc(volumeServer.StreamP2pOut), 49 | baggageclaim.DestroyVolume: http.HandlerFunc(volumeServer.DestroyVolume), 50 | baggageclaim.DestroyVolumes: http.HandlerFunc(volumeServer.DestroyVolumes), 51 | 52 | baggageclaim.GetP2pUrl: http.HandlerFunc(p2pServer.GetP2pUrl), 53 | } 54 | 55 | return rata.NewRouter(baggageclaim.Routes, handlers) 56 | } 57 | 58 | type ErrorResponse struct { 59 | Message string `json:"error"` 60 | } 61 | 62 | func RespondWithError(w http.ResponseWriter, err error, statusCode ...int) { 63 | var code int 64 | 65 | if len(statusCode) > 0 { 66 | code = statusCode[0] 67 | } else { 68 | code = http.StatusInternalServerError 69 | } 70 | 71 | w.WriteHeader(code) 72 | errResponse := ErrorResponse{Message: err.Error()} 73 | json.NewEncoder(w).Encode(errResponse) 74 | } 75 | -------------------------------------------------------------------------------- /api/p2p_server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "code.cloudfoundry.org/lager" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | var ErrGetP2pUrlFailed = errors.New("failed to get p2p url") 14 | 15 | func NewP2pServer( 16 | logger lager.Logger, 17 | p2pInterfacePattern *regexp.Regexp, 18 | p2pInterfaceFamily int, 19 | p2pStreamPort uint16, 20 | ) *P2pServer { 21 | return &P2pServer{ 22 | p2pInterfacePattern: p2pInterfacePattern, 23 | p2pInterfaceFamily: p2pInterfaceFamily, 24 | p2pStreamPort: p2pStreamPort, 25 | logger: logger, 26 | } 27 | } 28 | 29 | type P2pServer struct { 30 | p2pInterfacePattern *regexp.Regexp 31 | p2pInterfaceFamily int 32 | p2pStreamPort uint16 33 | 34 | logger lager.Logger 35 | } 36 | 37 | func (server *P2pServer) GetP2pUrl(w http.ResponseWriter, req *http.Request) { 38 | hLog := server.logger.Session("get-p2p-url") 39 | hLog.Debug("start") 40 | defer hLog.Debug("done") 41 | 42 | ifaces, err := net.Interfaces() 43 | if err != nil { 44 | RespondWithError(w, ErrGetP2pUrlFailed, http.StatusInternalServerError) 45 | return 46 | } 47 | 48 | for _, i := range ifaces { 49 | if !server.p2pInterfacePattern.MatchString(i.Name) { 50 | continue 51 | } 52 | 53 | addrs, err := i.Addrs() 54 | if err != nil { 55 | RespondWithError(w, ErrGetP2pUrlFailed, http.StatusInternalServerError) 56 | return 57 | } 58 | 59 | for _, addr := range addrs { 60 | var ip net.IP 61 | switch v := addr.(type) { 62 | case *net.IPNet: 63 | ip = v.IP 64 | case *net.IPAddr: 65 | ip = v.IP 66 | } 67 | 68 | if server.p2pInterfaceFamily == 6 { 69 | if strings.Contains(ip.String(), ".") { 70 | continue 71 | } 72 | } else { // Default to use IPv4 73 | if strings.Contains(ip.String(), ":") { 74 | continue 75 | } 76 | } 77 | hLog.Debug("found-ip", lager.Data{"ip": ip.String()}) 78 | 79 | fmt.Fprintf(w, "http://%s:%d", ip.String(), server.p2pStreamPort) 80 | return 81 | } 82 | } 83 | 84 | RespondWithError(w, ErrGetP2pUrlFailed, http.StatusInternalServerError) 85 | } 86 | -------------------------------------------------------------------------------- /api/p2p_server_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "regexp" 7 | "runtime" 8 | 9 | "code.cloudfoundry.org/lager/lagertest" 10 | "github.com/concourse/baggageclaim/api" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("P2P Server", func() { 16 | var ( 17 | handler http.Handler 18 | infc string 19 | ) 20 | 21 | JustBeforeEach(func() { 22 | var err error 23 | logger := lagertest.NewTestLogger("p2p-server") 24 | re := regexp.MustCompile(infc) 25 | handler, err = api.NewHandler(logger, nil, nil, re, 4, 7766) 26 | Expect(err).NotTo(HaveOccurred()) 27 | }) 28 | 29 | Describe("get p2p url", func() { 30 | var ( 31 | request *http.Request 32 | recorder *httptest.ResponseRecorder 33 | ) 34 | JustBeforeEach(func() { 35 | var err error 36 | request, err = http.NewRequest("GET", "/p2p-url", nil) 37 | Expect(err).NotTo(HaveOccurred()) 38 | 39 | recorder = httptest.NewRecorder() 40 | handler.ServeHTTP(recorder, request) 41 | }) 42 | 43 | Context("when a valid interface name is given", func() { 44 | BeforeEach(func() { 45 | infc = "lo" 46 | if runtime.GOOS == "windows" { 47 | infc = "Loopback" 48 | } 49 | }) 50 | 51 | It("returns a url successfully", func() { 52 | Expect(recorder.Code).To(Equal(200)) 53 | Expect(recorder.Body.String()).To(Equal("http://127.0.0.1:7766")) 54 | }) 55 | }) 56 | 57 | Context("when an invalid interface name is given", func() { 58 | BeforeEach(func() { 59 | infc = "dummy_interface" 60 | }) 61 | 62 | It("returns a url successfully", func() { 63 | Expect(recorder.Code).To(Equal(500)) 64 | }) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /api/query.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/concourse/baggageclaim/volume" 9 | ) 10 | 11 | func ConvertQueryToProperties(values url.Values) (volume.Properties, error) { 12 | properties := volume.Properties{} 13 | 14 | for name, value := range values { 15 | if len(value) > 1 { 16 | err := errors.New("a property may only have a single value: " + name + " has many (" + strings.Join(value, ", ") + ")") 17 | return volume.Properties{}, err 18 | } 19 | 20 | properties[name] = value[0] 21 | } 22 | 23 | return properties, nil 24 | } 25 | -------------------------------------------------------------------------------- /api/query_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "net/url" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | 9 | "github.com/concourse/baggageclaim/api" 10 | "github.com/concourse/baggageclaim/volume" 11 | ) 12 | 13 | var _ = Describe("Query Parameters", func() { 14 | It("returns the properties when each query parameter key only has one value", func() { 15 | values := url.Values{} 16 | values.Add("name1", "value1") 17 | values.Add("name2", "value2") 18 | values.Add("name3", "value3") 19 | 20 | properties, err := api.ConvertQueryToProperties(values) 21 | Expect(err).NotTo(HaveOccurred()) 22 | 23 | Expect(properties).To(Equal(volume.Properties{ 24 | "name1": "value1", 25 | "name2": "value2", 26 | "name3": "value3", 27 | })) 28 | 29 | }) 30 | 31 | It("returns an error when a query parameter has multiple values", func() { 32 | values := url.Values{} 33 | values.Add("name1", "value1") 34 | values.Add("name1", "value2") 35 | 36 | _, err := api.ConvertQueryToProperties(values) 37 | Expect(err).To(HaveOccurred()) 38 | }) 39 | 40 | It("returns empty properties when there are no query parameters", func() { 41 | values := url.Values{} 42 | 43 | properties, err := api.ConvertQueryToProperties(values) 44 | Expect(err).NotTo(HaveOccurred()) 45 | 46 | Expect(properties).To(BeEmpty()) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /api/volume_server_linux_test.go: -------------------------------------------------------------------------------- 1 | //+build linux 2 | 3 | package api_test 4 | 5 | import ( 6 | "archive/tar" 7 | "bytes" 8 | "compress/gzip" 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "net/http/httptest" 14 | "os" 15 | "path/filepath" 16 | "regexp" 17 | "syscall" 18 | 19 | . "github.com/onsi/ginkgo" 20 | . "github.com/onsi/gomega" 21 | 22 | "code.cloudfoundry.org/lager/lagertest" 23 | 24 | "github.com/concourse/baggageclaim" 25 | "github.com/concourse/baggageclaim/api" 26 | "github.com/concourse/baggageclaim/uidgid" 27 | "github.com/concourse/baggageclaim/volume" 28 | "github.com/concourse/baggageclaim/volume/driver" 29 | ) 30 | 31 | var _ = Describe("Volume Server", func() { 32 | var ( 33 | handler http.Handler 34 | 35 | volumeDir string 36 | tempDir string 37 | ) 38 | 39 | BeforeEach(func() { 40 | var err error 41 | 42 | tempDir, err = ioutil.TempDir("", fmt.Sprintf("baggageclaim_volume_dir_%d", GinkgoParallelNode())) 43 | Expect(err).NotTo(HaveOccurred()) 44 | 45 | // ioutil.TempDir creates it 0700; we need public readability for 46 | // unprivileged StreamIn 47 | err = os.Chmod(tempDir, 0755) 48 | Expect(err).NotTo(HaveOccurred()) 49 | 50 | volumeDir = tempDir 51 | }) 52 | 53 | JustBeforeEach(func() { 54 | logger := lagertest.NewTestLogger("volume-server") 55 | 56 | fs, err := volume.NewFilesystem(&driver.NaiveDriver{}, volumeDir) 57 | Expect(err).NotTo(HaveOccurred()) 58 | 59 | privilegedNamespacer := &uidgid.UidNamespacer{ 60 | Translator: uidgid.NewTranslator(uidgid.NewPrivilegedMapper()), 61 | Logger: logger.Session("uid-namespacer"), 62 | } 63 | 64 | unprivilegedNamespacer := &uidgid.UidNamespacer{ 65 | Translator: uidgid.NewTranslator(uidgid.NewUnprivilegedMapper()), 66 | Logger: logger.Session("uid-namespacer"), 67 | } 68 | 69 | repo := volume.NewRepository( 70 | fs, 71 | volume.NewLockManager(), 72 | privilegedNamespacer, 73 | unprivilegedNamespacer, 74 | ) 75 | 76 | strategerizer := volume.NewStrategerizer() 77 | 78 | re := regexp.MustCompile("eth0") 79 | handler, err = api.NewHandler(logger, strategerizer, repo, re, 4, 7766) 80 | Expect(err).NotTo(HaveOccurred()) 81 | }) 82 | 83 | AfterEach(func() { 84 | err := os.RemoveAll(tempDir + "/*") 85 | Expect(err).NotTo(HaveOccurred()) 86 | }) 87 | 88 | Describe("streaming tar files into volumes", func() { 89 | var ( 90 | myVolume volume.Volume 91 | tgzBuffer *bytes.Buffer 92 | isPrivileged bool 93 | ) 94 | 95 | JustBeforeEach(func() { 96 | body := &bytes.Buffer{} 97 | 98 | err := json.NewEncoder(body).Encode(baggageclaim.VolumeRequest{ 99 | Handle: "some-handle", 100 | Strategy: encStrategy(map[string]string{ 101 | "type": "empty", 102 | }), 103 | Privileged: isPrivileged, 104 | }) 105 | Expect(err).NotTo(HaveOccurred()) 106 | 107 | request, err := http.NewRequest("POST", "/volumes", body) 108 | Expect(err).NotTo(HaveOccurred()) 109 | 110 | recorder := httptest.NewRecorder() 111 | handler.ServeHTTP(recorder, request) 112 | Expect(recorder.Code).To(Equal(201)) 113 | 114 | err = json.NewDecoder(recorder.Body).Decode(&myVolume) 115 | Expect(err).NotTo(HaveOccurred()) 116 | }) 117 | 118 | Context("when tar file is valid", func() { 119 | BeforeEach(func() { 120 | tgzBuffer = new(bytes.Buffer) 121 | gzWriter := gzip.NewWriter(tgzBuffer) 122 | defer gzWriter.Close() 123 | 124 | tarWriter := tar.NewWriter(gzWriter) 125 | defer tarWriter.Close() 126 | 127 | err := tarWriter.WriteHeader(&tar.Header{ 128 | Name: "some-file", 129 | Mode: 0600, 130 | Size: int64(len("file-content")), 131 | }) 132 | Expect(err).NotTo(HaveOccurred()) 133 | _, err = tarWriter.Write([]byte("file-content")) 134 | Expect(err).NotTo(HaveOccurred()) 135 | }) 136 | 137 | Context("when volume is not privileged", func() { 138 | BeforeEach(func() { 139 | isPrivileged = false 140 | }) 141 | 142 | It("namespaces volume path", func() { 143 | request, _ := http.NewRequest("PUT", fmt.Sprintf("/volumes/%s/stream-in?path=%s", myVolume.Handle, "dest-path"), tgzBuffer) 144 | request.Header.Set("Content-Encoding", "gzip") 145 | recorder := httptest.NewRecorder() 146 | handler.ServeHTTP(recorder, request) 147 | Expect(recorder.Code).To(Equal(204)) 148 | 149 | tarInfoPath := filepath.Join(volumeDir, "live", myVolume.Handle, "volume", "dest-path", "some-file") 150 | Expect(tarInfoPath).To(BeAnExistingFile()) 151 | 152 | stat, err := os.Stat(tarInfoPath) 153 | Expect(err).ToNot(HaveOccurred()) 154 | 155 | maxUID := uidgid.MustGetMaxValidUID() 156 | maxGID := uidgid.MustGetMaxValidGID() 157 | 158 | sysStat := stat.Sys().(*syscall.Stat_t) 159 | Expect(sysStat.Uid).To(Equal(uint32(maxUID))) 160 | Expect(sysStat.Gid).To(Equal(uint32(maxGID))) 161 | }) 162 | }) 163 | 164 | Context("when volume privileged", func() { 165 | BeforeEach(func() { 166 | isPrivileged = true 167 | }) 168 | 169 | It("namespaces volume path", func() { 170 | request, _ := http.NewRequest("PUT", fmt.Sprintf("/volumes/%s/stream-in?path=%s", myVolume.Handle, "dest-path"), tgzBuffer) 171 | request.Header.Set("Content-Encoding", "gzip") 172 | recorder := httptest.NewRecorder() 173 | handler.ServeHTTP(recorder, request) 174 | Expect(recorder.Code).To(Equal(204)) 175 | 176 | tarInfoPath := filepath.Join(volumeDir, "live", myVolume.Handle, "volume", "dest-path", "some-file") 177 | Expect(tarInfoPath).To(BeAnExistingFile()) 178 | 179 | stat, err := os.Stat(tarInfoPath) 180 | Expect(err).ToNot(HaveOccurred()) 181 | 182 | sysStat := stat.Sys().(*syscall.Stat_t) 183 | Expect(sysStat.Uid).To(Equal(uint32(0))) 184 | Expect(sysStat.Gid).To(Equal(uint32(0))) 185 | }) 186 | }) 187 | }) 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /baggageclaimcmd/command.go: -------------------------------------------------------------------------------- 1 | package baggageclaimcmd 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "regexp" 8 | 9 | "code.cloudfoundry.org/lager" 10 | "github.com/concourse/baggageclaim/api" 11 | "github.com/concourse/baggageclaim/uidgid" 12 | "github.com/concourse/baggageclaim/volume" 13 | "github.com/concourse/flag" 14 | "github.com/tedsuo/ifrit" 15 | "github.com/tedsuo/ifrit/grouper" 16 | "github.com/tedsuo/ifrit/http_server" 17 | "github.com/tedsuo/ifrit/sigmon" 18 | ) 19 | 20 | type BaggageclaimCommand struct { 21 | Logger flag.Lager 22 | 23 | BindIP flag.IP `long:"bind-ip" default:"127.0.0.1" description:"IP address on which to listen for API traffic."` 24 | BindPort uint16 `long:"bind-port" default:"7788" description:"Port on which to listen for API traffic."` 25 | 26 | DebugBindIP flag.IP `long:"debug-bind-ip" default:"127.0.0.1" description:"IP address on which to listen for the pprof debugger endpoints."` 27 | DebugBindPort uint16 `long:"debug-bind-port" default:"7787" description:"Port on which to listen for the pprof debugger endpoints."` 28 | 29 | P2pInterfaceNamePattern string `long:"p2p-interface-name-pattern" default:"eth0" description:"Regular expression to match a network interface for p2p streaming"` 30 | P2pInterfaceFamily int `long:"p2p-interface-family" default:"4" choice:"4" choice:"6" description:"4 for IPv4 and 6 for IPv6"` 31 | 32 | VolumesDir flag.Dir `long:"volumes" required:"true" description:"Directory in which to place volume data."` 33 | 34 | Driver string `long:"driver" default:"detect" choice:"detect" choice:"naive" choice:"btrfs" choice:"overlay" description:"Driver to use for managing volumes."` 35 | 36 | BtrfsBin string `long:"btrfs-bin" default:"btrfs" description:"Path to btrfs binary"` 37 | MkfsBin string `long:"mkfs-bin" default:"mkfs.btrfs" description:"Path to mkfs.btrfs binary"` 38 | 39 | OverlaysDir string `long:"overlays-dir" description:"Path to directory in which to store overlay data"` 40 | 41 | DisableUserNamespaces bool `long:"disable-user-namespaces" description:"Disable remapping of user/group IDs in unprivileged volumes."` 42 | } 43 | 44 | func (cmd *BaggageclaimCommand) Execute(args []string) error { 45 | runner, err := cmd.Runner(args) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | return <-ifrit.Invoke(sigmon.New(runner)).Wait() 51 | } 52 | 53 | func (cmd *BaggageclaimCommand) Runner(args []string) (ifrit.Runner, error) { 54 | logger, _ := cmd.constructLogger() 55 | 56 | listenAddr := fmt.Sprintf("%s:%d", cmd.BindIP.IP, cmd.BindPort) 57 | 58 | var privilegedNamespacer, unprivilegedNamespacer uidgid.Namespacer 59 | 60 | if !cmd.DisableUserNamespaces && uidgid.Supported() { 61 | privilegedNamespacer = &uidgid.UidNamespacer{ 62 | Translator: uidgid.NewTranslator(uidgid.NewPrivilegedMapper()), 63 | Logger: logger.Session("uid-namespacer"), 64 | } 65 | 66 | unprivilegedNamespacer = &uidgid.UidNamespacer{ 67 | Translator: uidgid.NewTranslator(uidgid.NewUnprivilegedMapper()), 68 | Logger: logger.Session("uid-namespacer"), 69 | } 70 | } else { 71 | privilegedNamespacer = uidgid.NoopNamespacer{} 72 | unprivilegedNamespacer = uidgid.NoopNamespacer{} 73 | } 74 | 75 | locker := volume.NewLockManager() 76 | 77 | driver, err := cmd.driver(logger) 78 | if err != nil { 79 | logger.Error("failed-to-set-up-driver", err) 80 | return nil, err 81 | } 82 | 83 | filesystem, err := volume.NewFilesystem(driver, cmd.VolumesDir.Path()) 84 | if err != nil { 85 | logger.Error("failed-to-initialize-filesystem", err) 86 | return nil, err 87 | } 88 | 89 | err = driver.Recover(filesystem) 90 | if err != nil { 91 | logger.Error("failed-to-recover-volume-driver", err) 92 | return nil, err 93 | } 94 | 95 | volumeRepo := volume.NewRepository( 96 | filesystem, 97 | locker, 98 | privilegedNamespacer, 99 | unprivilegedNamespacer, 100 | ) 101 | 102 | re, err := regexp.Compile(cmd.P2pInterfaceNamePattern) 103 | if err != nil { 104 | logger.Error("failed-to-compile-p2p-interface-name-pattern", err) 105 | return nil, err 106 | } 107 | apiHandler, err := api.NewHandler( 108 | logger.Session("api"), 109 | volume.NewStrategerizer(), 110 | volumeRepo, 111 | re, 112 | cmd.P2pInterfaceFamily, 113 | cmd.BindPort, 114 | ) 115 | if err != nil { 116 | logger.Fatal("failed-to-create-handler", err) 117 | } 118 | 119 | members := []grouper.Member{ 120 | {Name: "api", Runner: http_server.New(listenAddr, apiHandler)}, 121 | {Name: "debug-server", Runner: http_server.New( 122 | cmd.debugBindAddr(), 123 | http.DefaultServeMux, 124 | )}, 125 | } 126 | 127 | return onReady(grouper.NewParallel(os.Interrupt, members), func() { 128 | logger.Info("listening", lager.Data{ 129 | "addr": listenAddr, 130 | }) 131 | }), nil 132 | } 133 | 134 | func (cmd *BaggageclaimCommand) constructLogger() (lager.Logger, *lager.ReconfigurableSink) { 135 | logger, reconfigurableSink := cmd.Logger.Logger("baggageclaim") 136 | 137 | return logger, reconfigurableSink 138 | } 139 | 140 | func (cmd *BaggageclaimCommand) debugBindAddr() string { 141 | return fmt.Sprintf("%s:%d", cmd.DebugBindIP, cmd.DebugBindPort) 142 | } 143 | 144 | func onReady(runner ifrit.Runner, cb func()) ifrit.Runner { 145 | return ifrit.RunFunc(func(signals <-chan os.Signal, ready chan<- struct{}) error { 146 | process := ifrit.Background(runner) 147 | 148 | subExited := process.Wait() 149 | subReady := process.Ready() 150 | 151 | for { 152 | select { 153 | case <-subReady: 154 | cb() 155 | subReady = nil 156 | case err := <-subExited: 157 | return err 158 | case sig := <-signals: 159 | process.Signal(sig) 160 | } 161 | } 162 | }) 163 | } 164 | -------------------------------------------------------------------------------- /baggageclaimcmd/driver_linux.go: -------------------------------------------------------------------------------- 1 | package baggageclaimcmd 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "os/exec" 11 | "syscall" 12 | 13 | "code.cloudfoundry.org/lager" 14 | "github.com/concourse/baggageclaim/fs" 15 | "github.com/concourse/baggageclaim/kernel" 16 | "github.com/concourse/baggageclaim/volume" 17 | "github.com/concourse/baggageclaim/volume/driver" 18 | ) 19 | 20 | const btrfsFSType = 0x9123683e 21 | 22 | func (cmd *BaggageclaimCommand) driver(logger lager.Logger) (volume.Driver, error) { 23 | var fsStat syscall.Statfs_t 24 | err := syscall.Statfs(cmd.VolumesDir.Path(), &fsStat) 25 | if err != nil { 26 | return nil, fmt.Errorf("failed to stat volumes filesystem: %s", err) 27 | } 28 | 29 | kernelSupportsOverlay, err := kernel.CheckKernelVersion(4, 0, 0) 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to check kernel version: %s", err) 32 | } 33 | 34 | // we don't care about the error here 35 | _ = exec.Command("modprobe", "btrfs").Run() 36 | 37 | supportsBtrfs, err := supportsFilesystem("btrfs") 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to detect if btrfs is supported: %s", err) 40 | } 41 | 42 | _, err = exec.LookPath(cmd.BtrfsBin) 43 | if err != nil { 44 | supportsBtrfs = false 45 | } 46 | 47 | _, err = exec.LookPath(cmd.MkfsBin) 48 | if err != nil { 49 | supportsBtrfs = false 50 | } 51 | 52 | if cmd.Driver == "detect" { 53 | if supportsBtrfs { 54 | cmd.Driver = "btrfs" 55 | } else if kernelSupportsOverlay { 56 | cmd.Driver = "overlay" 57 | } else { 58 | cmd.Driver = "naive" 59 | } 60 | } 61 | 62 | volumesDir := cmd.VolumesDir.Path() 63 | 64 | if cmd.Driver == "btrfs" && uint32(fsStat.Type) != btrfsFSType { 65 | volumesImage := volumesDir + ".img" 66 | filesystem := fs.New(logger.Session("fs"), volumesImage, volumesDir, cmd.MkfsBin) 67 | 68 | diskSize := fsStat.Blocks * uint64(fsStat.Bsize) 69 | mountSize := diskSize - (10 * 1024 * 1024 * 1024) 70 | if int64(mountSize) < 0 { 71 | mountSize = diskSize 72 | } 73 | 74 | err = filesystem.Create(mountSize) 75 | if err != nil { 76 | return nil, fmt.Errorf("failed to create btrfs filesystem: %s", err) 77 | } 78 | } 79 | 80 | if cmd.Driver == "overlay" && !kernelSupportsOverlay { 81 | return nil, errors.New("overlay driver requires kernel version >= 4.0.0") 82 | } 83 | 84 | logger.Info("using-driver", lager.Data{"driver": cmd.Driver}) 85 | 86 | var d volume.Driver 87 | switch cmd.Driver { 88 | case "overlay": 89 | d = driver.NewOverlayDriver(cmd.OverlaysDir) 90 | case "btrfs": 91 | d = driver.NewBtrFSDriver(logger.Session("driver"), cmd.BtrfsBin) 92 | case "naive": 93 | d = &driver.NaiveDriver{} 94 | default: 95 | return nil, fmt.Errorf("unknown driver: %s", cmd.Driver) 96 | } 97 | 98 | return d, nil 99 | } 100 | 101 | func supportsFilesystem(fs string) (bool, error) { 102 | filesystems, err := os.Open("/proc/filesystems") 103 | if err != nil { 104 | return false, err 105 | } 106 | 107 | defer filesystems.Close() 108 | 109 | fsio := bufio.NewReader(filesystems) 110 | 111 | fsMatch := []byte(fs) 112 | 113 | for { 114 | line, _, err := fsio.ReadLine() 115 | if err != nil { 116 | if err == io.EOF { 117 | return false, nil 118 | } 119 | 120 | return false, err 121 | } 122 | 123 | if bytes.Contains(line, fsMatch) { 124 | return true, nil 125 | } 126 | } 127 | 128 | return false, nil 129 | } 130 | -------------------------------------------------------------------------------- /baggageclaimcmd/driver_nonlinux.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | 3 | package baggageclaimcmd 4 | 5 | import ( 6 | "code.cloudfoundry.org/lager" 7 | "github.com/concourse/baggageclaim/volume" 8 | "github.com/concourse/baggageclaim/volume/driver" 9 | ) 10 | 11 | func (cmd *BaggageclaimCommand) driver(logger lager.Logger) (volume.Driver, error) { 12 | return &driver.NaiveDriver{}, nil 13 | } 14 | -------------------------------------------------------------------------------- /baggageclaimfakes/fake_volume_future.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package baggageclaimfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/concourse/baggageclaim" 8 | ) 9 | 10 | type FakeVolumeFuture struct { 11 | DestroyStub func() error 12 | destroyMutex sync.RWMutex 13 | destroyArgsForCall []struct { 14 | } 15 | destroyReturns struct { 16 | result1 error 17 | } 18 | destroyReturnsOnCall map[int]struct { 19 | result1 error 20 | } 21 | WaitStub func() (baggageclaim.Volume, error) 22 | waitMutex sync.RWMutex 23 | waitArgsForCall []struct { 24 | } 25 | waitReturns struct { 26 | result1 baggageclaim.Volume 27 | result2 error 28 | } 29 | waitReturnsOnCall map[int]struct { 30 | result1 baggageclaim.Volume 31 | result2 error 32 | } 33 | invocations map[string][][]interface{} 34 | invocationsMutex sync.RWMutex 35 | } 36 | 37 | func (fake *FakeVolumeFuture) Destroy() error { 38 | fake.destroyMutex.Lock() 39 | ret, specificReturn := fake.destroyReturnsOnCall[len(fake.destroyArgsForCall)] 40 | fake.destroyArgsForCall = append(fake.destroyArgsForCall, struct { 41 | }{}) 42 | fake.recordInvocation("Destroy", []interface{}{}) 43 | fake.destroyMutex.Unlock() 44 | if fake.DestroyStub != nil { 45 | return fake.DestroyStub() 46 | } 47 | if specificReturn { 48 | return ret.result1 49 | } 50 | fakeReturns := fake.destroyReturns 51 | return fakeReturns.result1 52 | } 53 | 54 | func (fake *FakeVolumeFuture) DestroyCallCount() int { 55 | fake.destroyMutex.RLock() 56 | defer fake.destroyMutex.RUnlock() 57 | return len(fake.destroyArgsForCall) 58 | } 59 | 60 | func (fake *FakeVolumeFuture) DestroyCalls(stub func() error) { 61 | fake.destroyMutex.Lock() 62 | defer fake.destroyMutex.Unlock() 63 | fake.DestroyStub = stub 64 | } 65 | 66 | func (fake *FakeVolumeFuture) DestroyReturns(result1 error) { 67 | fake.destroyMutex.Lock() 68 | defer fake.destroyMutex.Unlock() 69 | fake.DestroyStub = nil 70 | fake.destroyReturns = struct { 71 | result1 error 72 | }{result1} 73 | } 74 | 75 | func (fake *FakeVolumeFuture) DestroyReturnsOnCall(i int, result1 error) { 76 | fake.destroyMutex.Lock() 77 | defer fake.destroyMutex.Unlock() 78 | fake.DestroyStub = nil 79 | if fake.destroyReturnsOnCall == nil { 80 | fake.destroyReturnsOnCall = make(map[int]struct { 81 | result1 error 82 | }) 83 | } 84 | fake.destroyReturnsOnCall[i] = struct { 85 | result1 error 86 | }{result1} 87 | } 88 | 89 | func (fake *FakeVolumeFuture) Wait() (baggageclaim.Volume, error) { 90 | fake.waitMutex.Lock() 91 | ret, specificReturn := fake.waitReturnsOnCall[len(fake.waitArgsForCall)] 92 | fake.waitArgsForCall = append(fake.waitArgsForCall, struct { 93 | }{}) 94 | fake.recordInvocation("Wait", []interface{}{}) 95 | fake.waitMutex.Unlock() 96 | if fake.WaitStub != nil { 97 | return fake.WaitStub() 98 | } 99 | if specificReturn { 100 | return ret.result1, ret.result2 101 | } 102 | fakeReturns := fake.waitReturns 103 | return fakeReturns.result1, fakeReturns.result2 104 | } 105 | 106 | func (fake *FakeVolumeFuture) WaitCallCount() int { 107 | fake.waitMutex.RLock() 108 | defer fake.waitMutex.RUnlock() 109 | return len(fake.waitArgsForCall) 110 | } 111 | 112 | func (fake *FakeVolumeFuture) WaitCalls(stub func() (baggageclaim.Volume, error)) { 113 | fake.waitMutex.Lock() 114 | defer fake.waitMutex.Unlock() 115 | fake.WaitStub = stub 116 | } 117 | 118 | func (fake *FakeVolumeFuture) WaitReturns(result1 baggageclaim.Volume, result2 error) { 119 | fake.waitMutex.Lock() 120 | defer fake.waitMutex.Unlock() 121 | fake.WaitStub = nil 122 | fake.waitReturns = struct { 123 | result1 baggageclaim.Volume 124 | result2 error 125 | }{result1, result2} 126 | } 127 | 128 | func (fake *FakeVolumeFuture) WaitReturnsOnCall(i int, result1 baggageclaim.Volume, result2 error) { 129 | fake.waitMutex.Lock() 130 | defer fake.waitMutex.Unlock() 131 | fake.WaitStub = nil 132 | if fake.waitReturnsOnCall == nil { 133 | fake.waitReturnsOnCall = make(map[int]struct { 134 | result1 baggageclaim.Volume 135 | result2 error 136 | }) 137 | } 138 | fake.waitReturnsOnCall[i] = struct { 139 | result1 baggageclaim.Volume 140 | result2 error 141 | }{result1, result2} 142 | } 143 | 144 | func (fake *FakeVolumeFuture) Invocations() map[string][][]interface{} { 145 | fake.invocationsMutex.RLock() 146 | defer fake.invocationsMutex.RUnlock() 147 | fake.destroyMutex.RLock() 148 | defer fake.destroyMutex.RUnlock() 149 | fake.waitMutex.RLock() 150 | defer fake.waitMutex.RUnlock() 151 | copiedInvocations := map[string][][]interface{}{} 152 | for key, value := range fake.invocations { 153 | copiedInvocations[key] = value 154 | } 155 | return copiedInvocations 156 | } 157 | 158 | func (fake *FakeVolumeFuture) recordInvocation(key string, args []interface{}) { 159 | fake.invocationsMutex.Lock() 160 | defer fake.invocationsMutex.Unlock() 161 | if fake.invocations == nil { 162 | fake.invocations = map[string][][]interface{}{} 163 | } 164 | if fake.invocations[key] == nil { 165 | fake.invocations[key] = [][]interface{}{} 166 | } 167 | fake.invocations[key] = append(fake.invocations[key], args) 168 | } 169 | 170 | var _ baggageclaim.VolumeFuture = new(FakeVolumeFuture) 171 | -------------------------------------------------------------------------------- /ci/build-image.yml: -------------------------------------------------------------------------------- 1 | platform: linux 2 | params: 3 | CONTEXT: baggageclaim-dockerfile 4 | inputs: 5 | - name: baggageclaim-dockerfile 6 | outputs: 7 | - name: image 8 | caches: 9 | - path: cache 10 | run: 11 | path: build 12 | -------------------------------------------------------------------------------- /ci/pipeline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | resource_types: 3 | - name: slack-notifier 4 | type: registry-image 5 | source: {repository: mockersf/concourse-slack-notifier} 6 | 7 | jobs: 8 | - name: build-image 9 | public: true 10 | plan: 11 | - in_parallel: 12 | - get: baggageclaim-image-building 13 | trigger: true 14 | - get: golang-1.x 15 | trigger: true 16 | - get: ci 17 | - get: oci-build-task 18 | - task: build 19 | image: oci-build-task 20 | privileged: true 21 | file: baggageclaim-image-building/ci/build-image.yml 22 | input_mapping: {baggageclaim-dockerfile: baggageclaim-image-building} 23 | - put: baggageclaim-ci-image 24 | params: {image: image/image.tar} 25 | on_failure: 26 | put: notify 27 | params: 28 | mode: normal 29 | alert_type: failed 30 | 31 | - name: baggageclaim 32 | public: true 33 | serial: true 34 | plan: 35 | - in_parallel: 36 | - get: baggageclaim 37 | trigger: true 38 | - get: baggageclaim-ci-image 39 | passed: [build-image] 40 | - get: ci 41 | - in_parallel: 42 | - task: unit-linux 43 | image: baggageclaim-ci-image 44 | privileged: true 45 | file: baggageclaim/ci/unit-linux.yml 46 | - task: unit-darwin 47 | file: baggageclaim/ci/unit-darwin.yml 48 | - task: unit-windows 49 | file: baggageclaim/ci/unit-windows.yml 50 | on_failure: 51 | put: notify 52 | params: 53 | mode: normal 54 | alert_type: failed 55 | 56 | resources: 57 | - name: baggageclaim 58 | type: git 59 | icon: &git-icon github-circle 60 | source: 61 | uri: https://github.com/concourse/baggageclaim.git 62 | branch: master 63 | 64 | - name: baggageclaim-image-building 65 | type: git 66 | icon: *git-icon 67 | source: 68 | uri: https://github.com/concourse/baggageclaim.git 69 | branch: master 70 | paths: 71 | - Dockerfile 72 | - ci/build-image.yml 73 | 74 | - name: ci 75 | type: git 76 | icon: *git-icon 77 | source: 78 | uri: https://github.com/concourse/ci.git 79 | branch: master 80 | 81 | - name: baggageclaim-ci-image 82 | type: registry-image 83 | icon: docker 84 | source: 85 | repository: concourse/baggageclaim-ci 86 | tag: latest 87 | password: ((docker.password)) 88 | username: ((docker.username)) 89 | 90 | - name: golang-1.x 91 | type: registry-image 92 | icon: language-go 93 | source: 94 | repository: golang 95 | tag: 1 96 | 97 | - name: oci-build-task 98 | type: registry-image 99 | icon: docker 100 | source: {repository: vito/oci-build-task} 101 | 102 | - name: notify 103 | type: slack-notifier 104 | icon: slack 105 | source: 106 | url: ((slack_hook)) 107 | -------------------------------------------------------------------------------- /ci/prs-pipeline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | resource_types: 3 | - name: pull-request 4 | type: registry-image 5 | source: {repository: teliaoss/github-pr-resource} 6 | 7 | resources: 8 | - name: baggageclaim-pr 9 | type: pull-request 10 | icon: source-pull 11 | source: 12 | repository: concourse/baggageclaim 13 | access_token: ((pull_requests_access_token)) 14 | 15 | - name: baggageclaim-master 16 | type: git 17 | icon: github 18 | source: 19 | uri: https://github.com/concourse/baggageclaim 20 | 21 | - name: baggageclaim-ci 22 | type: registry-image 23 | icon: docker 24 | source: {repository: concourse/baggageclaim-ci} 25 | 26 | jobs: 27 | - name: unit 28 | public: true 29 | on_failure: 30 | put: baggageclaim-pr 31 | params: {path: baggageclaim-pr, status: failure, context: unit} 32 | tags: [pr] 33 | on_success: 34 | put: baggageclaim-pr 35 | params: {path: baggageclaim-pr, status: success, context: unit} 36 | tags: [pr] 37 | plan: 38 | - in_parallel: 39 | - get: baggageclaim-pr 40 | trigger: true 41 | version: every 42 | tags: [pr] 43 | - get: baggageclaim-master 44 | tags: [pr] 45 | - get: baggageclaim-ci 46 | tags: [pr] 47 | - put: baggageclaim-pr 48 | params: {path: baggageclaim-pr, status: pending, context: unit} 49 | tags: [pr] 50 | - task: unit-linux 51 | image: baggageclaim-ci 52 | privileged: true 53 | timeout: 1h 54 | file: baggageclaim-master/ci/unit-linux.yml 55 | input_mapping: {baggageclaim: baggageclaim-pr} 56 | tags: [pr] 57 | -------------------------------------------------------------------------------- /ci/scripts/unit-darwin: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ft=sh 3 | 4 | set -e -x 5 | 6 | export GOPATH=$PWD/gopath 7 | export PATH=$GOPATH/bin:$PATH 8 | 9 | cd baggageclaim 10 | 11 | go mod download 12 | 13 | go install github.com/onsi/ginkgo/ginkgo 14 | 15 | ginkgo -r -race -nodes 4 --failOnPending --randomizeAllSpecs --keepGoing -skip=":skip" "$@" 16 | -------------------------------------------------------------------------------- /ci/scripts/unit-linux: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ft=sh 3 | 4 | set -e -x 5 | 6 | export GOPATH=$PWD/gopath 7 | export PATH=$GOPATH/bin:$PATH 8 | 9 | function permit_device_control() { 10 | local devices_mount_info=$(cat /proc/self/cgroup | grep devices) 11 | 12 | if [ -z "$devices_mount_info" ]; then 13 | # cgroups not set up; must not be in a container 14 | return 15 | fi 16 | 17 | local devices_subsytems=$(echo $devices_mount_info | cut -d: -f2) 18 | local devices_subdir=$(echo $devices_mount_info | cut -d: -f3) 19 | 20 | if [ "$devices_subdir" = "/" ]; then 21 | # we're in the root devices cgroup; must not be in a container 22 | return 23 | fi 24 | 25 | cgroup_dir=/tmp/devices-cgroup 26 | 27 | if [ ! -e ${cgroup_dir} ]; then 28 | # mount our container's devices subsystem somewhere 29 | mkdir ${cgroup_dir} 30 | fi 31 | 32 | if ! mountpoint -q ${cgroup_dir}; then 33 | if ! mount -t cgroup -o $devices_subsytems none ${cgroup_dir}; then 34 | return 1 35 | fi 36 | fi 37 | 38 | # permit our cgroup to do everything with all devices 39 | # ignore failure in case something has already done this; echo appears to 40 | # return EINVAL, possibly because devices this affects are already in use 41 | echo a > ${cgroup_dir}${devices_subdir}/devices.allow || true 42 | } 43 | 44 | function containers_gone_wild() { 45 | for i in $(seq 64 67); do 46 | mknod -m 0660 /scratch/loop$i b 7 $i 47 | ln -s /scratch/loop$i /dev/loop$i 48 | done 49 | } 50 | 51 | function salt_earth() { 52 | for i in $(seq 64 67); do 53 | losetup -d /dev/loop$i > /dev/null 2>&1 || true 54 | done 55 | } 56 | 57 | permit_device_control 58 | containers_gone_wild 59 | trap salt_earth EXIT 60 | 61 | cd baggageclaim 62 | 63 | # /tmp is sometimes overlay (it doesn't have a dedicated mountpoint so it's 64 | # whatever / is), so point $TMPDIR to /scratch which we can trust to be 65 | # non-overlay for the overlay driver tests 66 | export TMPDIR=/scratch 67 | 68 | go mod download 69 | 70 | go install github.com/onsi/ginkgo/ginkgo 71 | 72 | ginkgo -r -race -nodes 4 --failOnPending --randomizeAllSpecs --keepGoing -skip=":skip" "$@" 73 | -------------------------------------------------------------------------------- /ci/scripts/unit-windows.bat: -------------------------------------------------------------------------------- 1 | set PATH=C:\Go\bin;C:\Program Files\Git\cmd;C:\ProgramData\chocolatey\lib\mingw\tools\install\mingw64\bin;C:\Program Files (x86)\Windows Resource Kits\Tools;%PATH% 2 | 3 | set GOPATH=%CD%\gopath 4 | set PATH=%CD%\gopath\bin;%PATH% 5 | 6 | cd .\baggageclaim 7 | 8 | go mod download 9 | 10 | go install github.com/onsi/ginkgo/ginkgo 11 | 12 | ginkgo -r -p 13 | -------------------------------------------------------------------------------- /ci/unit-darwin.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: darwin 3 | 4 | inputs: 5 | - name: baggageclaim 6 | 7 | caches: 8 | - path: gopath/ 9 | 10 | run: 11 | path: baggageclaim/ci/scripts/unit-darwin 12 | -------------------------------------------------------------------------------- /ci/unit-linux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: registry-image 6 | source: 7 | repository: concourse/baggageclaim-ci 8 | 9 | inputs: 10 | - name: baggageclaim 11 | 12 | caches: 13 | - path: gopath/ 14 | 15 | run: 16 | path: baggageclaim/ci/scripts/unit-linux 17 | -------------------------------------------------------------------------------- /ci/unit-windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: windows 3 | 4 | inputs: 5 | - name: baggageclaim 6 | 7 | run: 8 | path: baggageclaim/ci/scripts/unit-windows.bat 9 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package baggageclaim 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | 8 | "code.cloudfoundry.org/lager" 9 | ) 10 | 11 | type Encoding string 12 | 13 | const GzipEncoding Encoding = "gzip" 14 | const ZstdEncoding Encoding = "zstd" 15 | 16 | //go:generate counterfeiter . Client 17 | 18 | // Client represents a client connection to a BaggageClaim server. 19 | type Client interface { 20 | // CreateVolume will create a volume on the remote server. By passing in a 21 | // VolumeSpec with a different strategy you can choose the type of volume 22 | // that you want to create. 23 | // 24 | // You are required to pass in a logger to the call to retain context across 25 | // the library boundary. 26 | // 27 | // CreateVolume returns the volume that was created or an error as to why it 28 | // could not be created. 29 | CreateVolume(lager.Logger, string, VolumeSpec) (Volume, error) 30 | 31 | // ListVolumes lists the volumes that are present on the server. A 32 | // VolumeProperties object can be passed in to filter the volumes that are in 33 | // the response. 34 | // 35 | // You are required to pass in a logger to the call to retain context across 36 | // the library boundary. 37 | // 38 | // ListVolumes returns the volumes that were found or an error as to why they 39 | // could not be listed. 40 | ListVolumes(lager.Logger, VolumeProperties) (Volumes, error) 41 | 42 | // LookupVolume finds a volume that is present on the server. It takes a 43 | // string that corresponds to the Handle of the Volume. 44 | // 45 | // You are required to pass in a logger to the call to retain context across 46 | // the library boundary. 47 | // 48 | // LookupVolume returns a bool if the volume is found with the matching volume 49 | // or an error as to why the volume could not be found. 50 | LookupVolume(lager.Logger, string) (Volume, bool, error) 51 | 52 | // DestroyVolumes deletes the list of volumes that is present on the server. It takes 53 | // a string of volumes 54 | // 55 | // You are required to pass in a logger to the call to retain context across 56 | // the library boundary. 57 | // 58 | // DestroyVolumes returns an error if any of the volume deletion fails. It does not 59 | // return an error if volumes were not found on the server. 60 | // DestroyVolumes returns an error as to why one or more volumes could not be deleted. 61 | DestroyVolumes(lager.Logger, []string) error 62 | 63 | // DestroyVolume deletes the volume with the provided handle that is present on the server. 64 | // 65 | // You are required to pass in a logger to the call to retain context across 66 | // the library boundary. 67 | // 68 | // DestroyVolume returns an error if the volume deletion fails. It does not 69 | // return an error if the volume was not found on the server. 70 | DestroyVolume(lager.Logger, string) error 71 | } 72 | 73 | //go:generate counterfeiter . Volume 74 | 75 | // Volume represents a volume in the BaggageClaim system. 76 | type Volume interface { 77 | // Handle returns a per-server unique identifier for the volume. The URL of 78 | // the server and a handle is enough to universally identify a volume. 79 | Handle() string 80 | 81 | // Path returns the filesystem path to the volume on the server. This can be 82 | // supplied to other systems in order to let them use the volume. 83 | Path() string 84 | 85 | // SetProperty sets a property on the Volume. Properties can be used to 86 | // filter the results in the ListVolumes call above. 87 | SetProperty(key string, value string) error 88 | 89 | // SetPrivileged namespaces or un-namespaces the UID/GID ownership of the 90 | // volume's contents. 91 | SetPrivileged(bool) error 92 | 93 | // GetPrivileged returns a bool indicating if the volume is privileged. 94 | GetPrivileged() (bool, error) 95 | 96 | // StreamIn calls BaggageClaim API endpoint in order to initialize tarStream 97 | // to stream the contents of the Reader into this volume at the specified path. 98 | StreamIn(ctx context.Context, path string, encoding Encoding, tarStream io.Reader) error 99 | 100 | StreamOut(ctx context.Context, path string, encoding Encoding) (io.ReadCloser, error) 101 | 102 | // Properties returns the currently set properties for a Volume. An error is 103 | // returned if these could not be retrieved. 104 | Properties() (VolumeProperties, error) 105 | 106 | // Destroy removes the volume and its contents. Note that it does not 107 | // safeguard against child volumes being present. 108 | Destroy() error 109 | 110 | // GetStreamInP2pUrl returns a modified StreamIn URL for this volume. The 111 | // returned URL contains a hostname that is reachable by other baggageclaim 112 | // servers on the same network. The URL can be passed to another 113 | // baggageclaim server to stream the contents of its source volume into 114 | // this target volume. 115 | GetStreamInP2pUrl(ctx context.Context, path string) (string, error) 116 | 117 | // StreamP2pOut streams the contents of this volume directly to another 118 | // baggageclaim server on the same network. 119 | StreamP2pOut(ctx context.Context, path string, streamInURL string, encoding Encoding) error 120 | } 121 | 122 | //go:generate counterfeiter . VolumeFuture 123 | 124 | type VolumeFuture interface { 125 | // Wait will wait until the future has been provided with a value, which is 126 | // either the volume that was created or an error as to why it could not be 127 | // created. 128 | Wait() (Volume, error) 129 | 130 | // Destroy removes the future from the remote server. This can be used to 131 | // either stop waiting for a value, or remove the value from the remote 132 | // server after it is no longer needed. 133 | Destroy() error 134 | } 135 | 136 | // Volumes represents a list of Volume object. 137 | type Volumes []Volume 138 | 139 | func (v Volumes) Handles() []string { 140 | var handles []string 141 | for _, vol := range v { 142 | handles = append(handles, vol.Handle()) 143 | } 144 | return handles 145 | } 146 | 147 | // VolumeProperties represents the properties for a particular volume. 148 | type VolumeProperties map[string]string 149 | 150 | // VolumeSpec is a specification representing the kind of volume that you'd 151 | // like from the server. 152 | type VolumeSpec struct { 153 | // Strategy is the information that the server requires to materialise the 154 | // volume. There are examples of these in this package. 155 | Strategy Strategy 156 | 157 | // Properties is the set of initial properties that the Volume should have. 158 | Properties VolumeProperties 159 | 160 | // Privileged is used to determine whether or not we need to perform a UID 161 | // translation of the files in the volume so that they can be read by a 162 | // non-privileged user. 163 | Privileged bool 164 | } 165 | 166 | type Strategy interface { 167 | Encode() *json.RawMessage 168 | } 169 | 170 | // ImportStrategy creates a volume by copying a directory from the host. 171 | type ImportStrategy struct { 172 | // The location on the host to import. If the path is a directory, its 173 | // contents will be copied in. If the path is a file, it is assumed to be a 174 | // .tar.gz file, and its contents will be unpacked in to the volume. 175 | Path string 176 | 177 | // Follow symlinks and import them as files instead of links. 178 | FollowSymlinks bool 179 | } 180 | 181 | func (strategy ImportStrategy) Encode() *json.RawMessage { 182 | payload, _ := json.Marshal(struct { 183 | Type string `json:"type"` 184 | Path string `json:"path"` 185 | FollowSymlinks bool `json:"follow_symlinks"` 186 | }{ 187 | Type: "import", 188 | Path: strategy.Path, 189 | FollowSymlinks: strategy.FollowSymlinks, 190 | }) 191 | 192 | msg := json.RawMessage(payload) 193 | return &msg 194 | } 195 | 196 | // COWStrategy creates a Copy-On-Write layer of another Volume. 197 | type COWStrategy struct { 198 | // The parent volume that we should base the new volume on. 199 | Parent Volume 200 | } 201 | 202 | func (strategy COWStrategy) Encode() *json.RawMessage { 203 | payload, _ := json.Marshal(struct { 204 | Type string `json:"type"` 205 | Volume string `json:"volume"` 206 | }{ 207 | Type: "cow", 208 | Volume: strategy.Parent.Handle(), 209 | }) 210 | 211 | msg := json.RawMessage(payload) 212 | return &msg 213 | } 214 | 215 | // EmptyStrategy created a new empty volume. 216 | type EmptyStrategy struct{} 217 | 218 | func (EmptyStrategy) Encode() *json.RawMessage { 219 | msg := json.RawMessage(`{"type":"empty"}`) 220 | return &msg 221 | } 222 | -------------------------------------------------------------------------------- /client/client_resiliency_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "sync" 7 | "time" 8 | 9 | "code.cloudfoundry.org/lager" 10 | "github.com/concourse/baggageclaim" 11 | "github.com/concourse/baggageclaim/client" 12 | "github.com/concourse/baggageclaim/volume" 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | "github.com/onsi/gomega/ghttp" 16 | ) 17 | 18 | var _ = Describe("baggageclaim http client", func() { 19 | Context("when making a streamIn/streamOut api call ", func() { 20 | var ( 21 | gServer *ghttp.Server 22 | ctx context.Context 23 | cancelFunc context.CancelFunc 24 | ) 25 | 26 | BeforeEach(func() { 27 | gServer = ghttp.NewServer() 28 | ctx, cancelFunc = context.WithCancel(context.Background()) 29 | }) 30 | 31 | AfterEach(func() { 32 | cancelFunc() 33 | gServer.Close() 34 | }) 35 | 36 | Context("when context is canceled", func() { 37 | 38 | BeforeEach(func() { 39 | gServer.Reset() 40 | gServer.AppendHandlers( 41 | ghttp.CombineHandlers( 42 | ghttp.VerifyRequest("POST", "/volumes-async"), 43 | ghttp.RespondWithJSONEncoded(http.StatusCreated, baggageclaim.VolumeFutureResponse{ 44 | Handle: "some-volume", 45 | }), 46 | ), 47 | ghttp.CombineHandlers( 48 | ghttp.VerifyRequest("GET", "/volumes-async/some-volume"), 49 | ghttp.RespondWithJSONEncoded(http.StatusOK, volume.Volume{ 50 | "some-volume", 51 | "/some/path", 52 | map[string]string{}, 53 | false, 54 | }), 55 | ), 56 | ghttp.CombineHandlers( 57 | ghttp.VerifyRequest("DELETE", "/volumes-async/some-volume"), 58 | ghttp.RespondWith(http.StatusOK, nil), 59 | ), 60 | ghttp.CombineHandlers( 61 | ghttp.VerifyRequest("PUT", "/volumes/some-volume/stream-out"), 62 | func(writer http.ResponseWriter, r *http.Request) { 63 | <-ctx.Done() 64 | }, 65 | ), 66 | ) 67 | }) 68 | 69 | It("should stop streaming and end the request with an error", func() { 70 | 71 | c := client.New(gServer.URL(), http.DefaultTransport) 72 | 73 | volume, err := c.CreateVolume(lager.NewLogger("test"), "some-volume", baggageclaim.VolumeSpec{Properties: map[string]string{}, Privileged: false}) 74 | Expect(err).ToNot(HaveOccurred()) 75 | 76 | var wg sync.WaitGroup 77 | requestCtx, cancelStream := context.WithCancel(context.Background()) 78 | 79 | wg.Add(1) 80 | go func() { 81 | _, err = volume.StreamOut(requestCtx, ".", "gzip") 82 | Expect(err).To(HaveOccurred()) 83 | Expect(err.Error()).To(ContainSubstring("context canceled")) 84 | 85 | wg.Done() 86 | }() 87 | 88 | time.AfterFunc(100*time.Millisecond, cancelStream) 89 | 90 | wg.Wait() 91 | }) 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /client/client_suite_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestClient(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Client Suite") 13 | } 14 | -------------------------------------------------------------------------------- /client/volume.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "code.cloudfoundry.org/lager" 8 | "github.com/concourse/baggageclaim" 9 | "github.com/concourse/baggageclaim/volume" 10 | ) 11 | 12 | type clientVolume struct { 13 | // TODO: this would be much better off as an arg to each method 14 | logger lager.Logger 15 | 16 | handle string 17 | path string 18 | 19 | bcClient *client 20 | } 21 | 22 | func (cv *clientVolume) Handle() string { 23 | return cv.handle 24 | } 25 | 26 | func (cv *clientVolume) Path() string { 27 | return cv.path 28 | } 29 | 30 | func (cv *clientVolume) Properties() (baggageclaim.VolumeProperties, error) { 31 | vr, found, err := cv.bcClient.getVolumeResponse(cv.logger, cv.handle) 32 | if err != nil { 33 | return nil, err 34 | } 35 | if !found { 36 | return nil, volume.ErrVolumeDoesNotExist 37 | } 38 | 39 | return vr.Properties, nil 40 | } 41 | 42 | func (cv *clientVolume) StreamIn(ctx context.Context, path string, encoding baggageclaim.Encoding, tarStream io.Reader) error { 43 | return cv.bcClient.streamIn(ctx, cv.logger, cv.handle, path, encoding, tarStream) 44 | } 45 | 46 | func (cv *clientVolume) StreamOut(ctx context.Context, path string, encoding baggageclaim.Encoding) (io.ReadCloser, error) { 47 | return cv.bcClient.streamOut(ctx, cv.logger, cv.handle, encoding, path) 48 | } 49 | 50 | func (cv *clientVolume) GetPrivileged() (bool, error) { 51 | return cv.bcClient.getPrivileged(cv.logger, cv.handle) 52 | } 53 | 54 | func (cv *clientVolume) SetPrivileged(privileged bool) error { 55 | return cv.bcClient.setPrivileged(cv.logger, cv.handle, privileged) 56 | } 57 | 58 | func (cv *clientVolume) Destroy() error { 59 | return cv.bcClient.destroy(cv.logger, cv.handle) 60 | } 61 | 62 | func (cv *clientVolume) SetProperty(name string, value string) error { 63 | return cv.bcClient.setProperty(cv.logger, cv.handle, name, value) 64 | } 65 | 66 | func (cv *clientVolume) GetStreamInP2pUrl(ctx context.Context, path string) (string, error) { 67 | return cv.bcClient.getStreamInP2pUrl(ctx, cv.logger, cv.handle, path) 68 | } 69 | 70 | func (cv *clientVolume) StreamP2pOut(ctx context.Context, path, url string, encoding baggageclaim.Encoding) error { 71 | return cv.bcClient.streamP2pOut(ctx, cv.logger, cv.handle, encoding, path, url) 72 | } 73 | -------------------------------------------------------------------------------- /client/volume_future.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "code.cloudfoundry.org/lager" 10 | "github.com/cenkalti/backoff" 11 | "github.com/tedsuo/rata" 12 | 13 | "github.com/concourse/baggageclaim" 14 | ) 15 | 16 | type volumeFuture struct { 17 | client *client 18 | handle string 19 | logger lager.Logger 20 | } 21 | 22 | func (f *volumeFuture) Wait() (baggageclaim.Volume, error) { 23 | request, err := f.client.requestGenerator.CreateRequest(baggageclaim.CreateVolumeAsyncCheck, rata.Params{ 24 | "handle": f.handle, 25 | }, nil) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | exponentialBackoff := backoff.NewExponentialBackOff() 31 | exponentialBackoff.InitialInterval = 10 * time.Millisecond 32 | exponentialBackoff.MaxInterval = 10 * time.Second 33 | exponentialBackoff.MaxElapsedTime = 0 34 | 35 | for { 36 | response, err := f.client.httpClient(f.logger).Do(request) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | if response.StatusCode == http.StatusNoContent { 42 | response.Body.Close() 43 | 44 | time.Sleep(exponentialBackoff.NextBackOff()) 45 | 46 | continue 47 | } 48 | 49 | defer response.Body.Close() 50 | 51 | if response.StatusCode != http.StatusOK { 52 | if response.StatusCode == http.StatusNotFound { 53 | return nil, fmt.Errorf("future not found: %s", f.handle) 54 | } 55 | return nil, getError(response) 56 | } 57 | 58 | if header := response.Header.Get("Content-Type"); header != "application/json" { 59 | return nil, fmt.Errorf("unexpected content-type of: %s", header) 60 | } 61 | 62 | var volumeResponse baggageclaim.VolumeResponse 63 | err = json.NewDecoder(response.Body).Decode(&volumeResponse) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return f.client.newVolume(f.logger, volumeResponse), nil 69 | } 70 | } 71 | 72 | func (f *volumeFuture) Destroy() error { 73 | request, err := f.client.requestGenerator.CreateRequest(baggageclaim.CreateVolumeAsyncCancel, rata.Params{ 74 | "handle": f.handle, 75 | }, nil) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | response, err := f.client.httpClient(f.logger).Do(request) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | defer response.Body.Close() 86 | 87 | if response.StatusCode != http.StatusOK { 88 | if response.StatusCode == http.StatusNotFound { 89 | return fmt.Errorf("future not found: %s", f.handle) 90 | } 91 | return getError(response) 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /cmd/baggageclaim/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/concourse/baggageclaim/baggageclaimcmd" 8 | "github.com/jessevdk/go-flags" 9 | ) 10 | 11 | func main() { 12 | cmd := &baggageclaimcmd.BaggageclaimCommand{} 13 | 14 | parser := flags.NewParser(cmd, flags.Default) 15 | parser.NamespaceDelimiter = "-" 16 | 17 | args, err := parser.Parse() 18 | if err != nil { 19 | os.Exit(1) 20 | } 21 | 22 | err = cmd.Execute(args) 23 | if err != nil { 24 | fmt.Fprintln(os.Stderr, err) 25 | os.Exit(1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cmd/fs_mounter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "code.cloudfoundry.org/lager" 9 | "github.com/concourse/baggageclaim/fs" 10 | "github.com/jessevdk/go-flags" 11 | ) 12 | 13 | type FSMounterCommand struct { 14 | DiskImage string `long:"disk-image" required:"true" description:"Location of the backing file to create for the image."` 15 | 16 | MountPath string `long:"mount-path" required:"true" description:"Directory where the filesystem should be mounted."` 17 | 18 | SizeInMegabytes uint64 `long:"size-in-megabytes" default:"0" description:"Maximum size of the filesystem. Can exceed the size of the backing device."` 19 | 20 | Remove bool `long:"remove" description:"Remove the filesystem instead of creating it."` 21 | 22 | MkfsBin string `long:"mkfs-bin" default:"mkfs.btrfs" description:"Path to mkfs.btrfs binary"` 23 | } 24 | 25 | func main() { 26 | cmd := &FSMounterCommand{} 27 | 28 | parser := flags.NewParser(cmd, flags.Default) 29 | parser.NamespaceDelimiter = "-" 30 | 31 | _, err := parser.Parse() 32 | if err != nil { 33 | os.Exit(1) 34 | } 35 | 36 | logger := lager.NewLogger("baggageclaim") 37 | sink := lager.NewWriterSink(os.Stdout, lager.DEBUG) 38 | logger.RegisterSink(sink) 39 | 40 | filesystem := fs.New(logger, cmd.DiskImage, cmd.MountPath, cmd.MkfsBin) 41 | 42 | if !cmd.Remove { 43 | if cmd.SizeInMegabytes == 0 { 44 | fmt.Fprintln(os.Stderr, "--size-in-megabytes or --remove must be specified") 45 | os.Exit(1) 46 | } 47 | 48 | err := filesystem.Create(cmd.SizeInMegabytes * 1024 * 1024) 49 | if err != nil { 50 | log.Fatalln("failed to create filesystem: ", err) 51 | } 52 | } else { 53 | err := filesystem.Delete() 54 | if err != nil { 55 | log.Fatalln("failed to delete filesystem: ", err) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package baggageclaim is the interface for communicating with a BaggageClaim 3 | volume server. 4 | 5 | BaggageClaim is an auxilary service that can be collocated with various 6 | container servers (Garden, Docker, etc.) to let them share directories. 7 | BaggageClaim provides a number of benefits over regular bind mounts: 8 | 9 | By bringing everything into a the same Volume model we can compose different 10 | technologies together. For example, a Docker image is a stack of layered 11 | volumes which can have a Concourse build cache layered on top of them. 12 | 13 | Volumes can be Copy-on-Write (COW) copies of other volumes. This lets us 14 | download a Docker image once and then let it be used by untrusted jobs without 15 | fear that they'll mutate it in some unexpected way. This same COW strategy can 16 | be applied to any volume that BaggageClaim supports. 17 | 18 | BaggageClaim volumes go through a three stage lifecycle of being born, 19 | existing, and then dying. This state model is required as creating large 20 | amounts of data can potentially take a long time to materialize. You are only 21 | able to interact with volumes that are in the middle state. 22 | 23 | It's the responsibility of the API consumer to delete child volumes before 24 | parent volumes. 25 | 26 | The standard way to construct a client is: 27 | 28 | import "github.com/concourse/baggageclaim/client" 29 | 30 | bcClient := client.New("http://baggageclaim.example.com:7788") 31 | bcClient.CreateVolume(...) 32 | */ 33 | package baggageclaim 34 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package baggageclaim 2 | 3 | import "errors" 4 | 5 | var ErrVolumeNotFound = errors.New("volume not found") 6 | var ErrFileNotFound = errors.New("file not found") 7 | -------------------------------------------------------------------------------- /fs/btrfs.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | "code.cloudfoundry.org/lager" 11 | ) 12 | 13 | type BtrfsFilesystem struct { 14 | imagePath string 15 | mountPath string 16 | mkfsBin string 17 | 18 | logger lager.Logger 19 | } 20 | 21 | func New(logger lager.Logger, imagePath string, mountPath string, mkfsBin string) *BtrfsFilesystem { 22 | return &BtrfsFilesystem{ 23 | imagePath: imagePath, 24 | mountPath: mountPath, 25 | mkfsBin: mkfsBin, 26 | logger: logger, 27 | } 28 | } 29 | 30 | // lower your expectations 31 | func (fs *BtrfsFilesystem) Create(bytes uint64) error { 32 | 33 | // significantly 34 | idempotent := exec.Command("bash", "-e", "-x", "-c", ` 35 | if [ ! -e $IMAGE_PATH ] || [ "$(stat --printf="%s" $IMAGE_PATH)" != "$SIZE_IN_BYTES" ]; then 36 | touch $IMAGE_PATH 37 | truncate -s ${SIZE_IN_BYTES} $IMAGE_PATH 38 | fi 39 | 40 | lo="$(losetup -j $IMAGE_PATH | cut -d':' -f1)" 41 | if [ -z "$lo" ]; then 42 | lo="$(losetup -f --show $IMAGE_PATH)" 43 | fi 44 | 45 | if ! file $IMAGE_PATH | grep BTRFS; then 46 | `+fs.mkfsBin+` --nodiscard $IMAGE_PATH 47 | fi 48 | 49 | mkdir -p $MOUNT_PATH 50 | 51 | if ! mountpoint -q $MOUNT_PATH; then 52 | mount -t btrfs -o discard $lo $MOUNT_PATH 53 | fi 54 | `) 55 | 56 | idempotent.Env = []string{ 57 | "PATH=" + os.Getenv("PATH"), 58 | "MOUNT_PATH=" + fs.mountPath, 59 | "IMAGE_PATH=" + fs.imagePath, 60 | fmt.Sprintf("SIZE_IN_BYTES=%d", bytes), 61 | } 62 | 63 | _, err := fs.run(idempotent) 64 | return err 65 | } 66 | 67 | func (fs *BtrfsFilesystem) Delete() error { 68 | _, err := fs.run(exec.Command( 69 | "umount", 70 | fs.mountPath, 71 | )) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | if err := os.RemoveAll(fs.mountPath); err != nil { 77 | return err 78 | } 79 | 80 | loopbackOutput, err := fs.run(exec.Command( 81 | "losetup", 82 | "-j", 83 | fs.imagePath, 84 | )) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | loopbackDevice := strings.Split(loopbackOutput, ":")[0] 90 | 91 | _, err = fs.run(exec.Command( 92 | "losetup", 93 | "-d", 94 | loopbackDevice, 95 | )) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | return os.Remove(fs.imagePath) 101 | } 102 | 103 | func (fs *BtrfsFilesystem) run(cmd *exec.Cmd) (string, error) { 104 | logger := fs.logger.Session("run-command", lager.Data{ 105 | "command": cmd.Path, 106 | "args": cmd.Args, 107 | "env": cmd.Env, 108 | }) 109 | 110 | stdout := &bytes.Buffer{} 111 | stderr := &bytes.Buffer{} 112 | 113 | cmd.Stdout = stdout 114 | cmd.Stderr = stderr 115 | 116 | err := cmd.Run() 117 | 118 | loggerData := lager.Data{ 119 | "stdout": stdout.String(), 120 | "stderr": stderr.String(), 121 | } 122 | 123 | if err != nil { 124 | logger.Error("failed", err, loggerData) 125 | return "", err 126 | } 127 | 128 | logger.Debug("ran", loggerData) 129 | 130 | return stdout.String(), nil 131 | } 132 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/concourse/baggageclaim 2 | 3 | require ( 4 | code.cloudfoundry.org/lager v1.1.0 5 | github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect 6 | github.com/cenkalti/backoff v2.1.1+incompatible 7 | github.com/concourse/flag v0.0.0-20180907155614-cb47f24fff1c 8 | github.com/concourse/go-archive v0.0.0-20180803203406-784931698f4f 9 | github.com/concourse/retryhttp v0.0.0-20170802173037-937335fd9545 10 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 11 | github.com/google/go-cmp v0.4.0 // indirect 12 | github.com/jessevdk/go-flags v1.4.0 13 | github.com/klauspost/compress v1.9.7 14 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d 15 | github.com/onsi/ginkgo v1.8.0 16 | github.com/onsi/gomega v1.5.0 17 | github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc 18 | github.com/tedsuo/rata v1.0.1-0.20170830210128-07d200713958 19 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b // indirect 20 | golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3 // indirect 21 | golang.org/x/sys v0.0.0-20180918153733-ee1b12c67af4 22 | ) 23 | 24 | go 1.13 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | code.cloudfoundry.org/lager v1.1.0 h1:v0RELJ2jqTeF2DW7PNjZaaGlrXbVxJBVz3uLxdP3fuY= 2 | code.cloudfoundry.org/lager v1.1.0/go.mod h1:O2sS7gKP3HM2iemG+EnwvyNQK7pTSC6Foi4QiMp9sSk= 3 | github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 h1:y4B3+GPxKlrigF1ha5FFErxK+sr6sWxQovRMzwMhejo= 4 | github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= 5 | github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= 6 | github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 7 | github.com/concourse/flag v0.0.0-20180907155614-cb47f24fff1c h1:SwMKBc36jGTSS5/3AqVtbRjbGTvDfR3pJ5MkzfiYSas= 8 | github.com/concourse/flag v0.0.0-20180907155614-cb47f24fff1c/go.mod h1:ngs845OZCESOe8vgeK5fsCNIiS0vUSqB8MGQMS9+4og= 9 | github.com/concourse/go-archive v0.0.0-20180803203406-784931698f4f h1:Z7s60kZd9kuWiosivO9QPGH1vmfC/lzLDiuWIy0MT+o= 10 | github.com/concourse/go-archive v0.0.0-20180803203406-784931698f4f/go.mod h1:Xfo080IPQBmVz3I5ehjCddW3phA2mwv0NFwlpjf5CO8= 11 | github.com/concourse/retryhttp v0.0.0-20170802173037-937335fd9545 h1:EprZV3ig+v+5sOp0eruDCgrW/nlmyXm4Z8JOF1Unz1o= 12 | github.com/concourse/retryhttp v0.0.0-20170802173037-937335fd9545/go.mod h1:4+V9YCkKuoV7rg+/No+ZM9FsO3BK4tIJNUiYMI7nki0= 13 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 14 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 15 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 16 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 17 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 18 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 19 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 20 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 21 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 22 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 23 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 24 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 25 | github.com/klauspost/compress v1.9.7 h1:hYW1gP94JUmAhBtJ+LNz5My+gBobDxPR1iVuKug26aA= 26 | github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 27 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= 28 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 29 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 30 | github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= 31 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 32 | github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= 33 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 34 | github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc h1:LUUe4cdABGrIJAhl1P1ZpWY76AwukVszFdwkVFVLwIk= 35 | github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= 36 | github.com/tedsuo/rata v1.0.1-0.20170830210128-07d200713958 h1:mueRRuRjR35dEOkHdhpoRcruNgBz0ohG659HxxmcAwA= 37 | github.com/tedsuo/rata v1.0.1-0.20170830210128-07d200713958/go.mod h1:X47ELzhOoLbfFIY0Cql9P6yo3Cdwf2CMX3FVZxRzJPc= 38 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ= 39 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 40 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 41 | golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3 h1:czFLhve3vsQetD6JOJ8NZZvGQIXlnN3/yXxbT6/awxI= 42 | golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 43 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 44 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 45 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 46 | golang.org/x/sys v0.0.0-20180918153733-ee1b12c67af4 h1:h8ij2QOL81JqJ/Vi5Ru+hl4a1yct8+XDGrgBhG0XbuE= 47 | golang.org/x/sys v0.0.0-20180918153733-ee1b12c67af4/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 49 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 50 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 51 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 55 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 56 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 57 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 58 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 59 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 60 | -------------------------------------------------------------------------------- /integration/baggageclaim/copy_on_write_strategy_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "github.com/concourse/baggageclaim" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("Copy On Write Strategy", func() { 10 | var ( 11 | runner *BaggageClaimRunner 12 | client baggageclaim.Client 13 | ) 14 | 15 | BeforeEach(func() { 16 | runner = NewRunner(baggageClaimPath, "naive") 17 | runner.Start() 18 | client = runner.Client() 19 | }) 20 | 21 | AfterEach(func() { 22 | runner.Stop() 23 | runner.Cleanup() 24 | }) 25 | 26 | Describe("API", func() { 27 | Describe("POST /volumes with strategy: cow", func() { 28 | It("creates a copy of the volume", func() { 29 | parentVolume, err := client.CreateVolume(logger, "some-handle", baggageclaim.VolumeSpec{}) 30 | Expect(err).NotTo(HaveOccurred()) 31 | 32 | dataInParent := writeData(parentVolume.Path()) 33 | Expect(dataExistsInVolume(dataInParent, parentVolume.Path())).To(BeTrue()) 34 | 35 | childVolume, err := client.CreateVolume(logger, "another-handle", baggageclaim.VolumeSpec{ 36 | Strategy: baggageclaim.COWStrategy{ 37 | Parent: parentVolume, 38 | }, 39 | }) 40 | Expect(err).NotTo(HaveOccurred()) 41 | 42 | Expect(dataExistsInVolume(dataInParent, childVolume.Path())).To(BeTrue()) 43 | 44 | newDataInParent := writeData(parentVolume.Path()) 45 | Expect(dataExistsInVolume(newDataInParent, parentVolume.Path())).To(BeTrue()) 46 | Expect(dataExistsInVolume(newDataInParent, childVolume.Path())).To(BeFalse()) 47 | 48 | dataInChild := writeData(childVolume.Path()) 49 | Expect(dataExistsInVolume(dataInChild, childVolume.Path())).To(BeTrue()) 50 | Expect(dataExistsInVolume(dataInChild, parentVolume.Path())).To(BeFalse()) 51 | }) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /integration/baggageclaim/destroy_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/concourse/baggageclaim" 8 | ) 9 | 10 | var _ = Describe("Destroying", func() { 11 | var ( 12 | runner *BaggageClaimRunner 13 | client baggageclaim.Client 14 | ) 15 | 16 | BeforeEach(func() { 17 | runner = NewRunner(baggageClaimPath, "naive") 18 | runner.Start() 19 | 20 | client = runner.Client() 21 | }) 22 | 23 | AfterEach(func() { 24 | runner.Stop() 25 | runner.Cleanup() 26 | }) 27 | 28 | It("destroys volume", func() { 29 | createdVolume, err := client.CreateVolume(logger, "some-handle", baggageclaim.VolumeSpec{}) 30 | Expect(err).NotTo(HaveOccurred()) 31 | 32 | Expect(runner.CurrentHandles()).To(ConsistOf(createdVolume.Handle())) 33 | 34 | err = createdVolume.Destroy() 35 | Expect(err).NotTo(HaveOccurred()) 36 | 37 | Expect(runner.CurrentHandles()).NotTo(ConsistOf(createdVolume.Handle())) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /integration/baggageclaim/empty_strategy_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/concourse/baggageclaim" 8 | ) 9 | 10 | var _ = Describe("Empty Strategy", func() { 11 | var ( 12 | runner *BaggageClaimRunner 13 | client baggageclaim.Client 14 | ) 15 | 16 | BeforeEach(func() { 17 | runner = NewRunner(baggageClaimPath, "naive") 18 | runner.Start() 19 | client = runner.Client() 20 | }) 21 | 22 | AfterEach(func() { 23 | runner.Stop() 24 | runner.Cleanup() 25 | }) 26 | 27 | Describe("API", func() { 28 | properties := baggageclaim.VolumeProperties{ 29 | "name": "value", 30 | } 31 | 32 | Describe("POST /volumes", func() { 33 | var ( 34 | firstVolume baggageclaim.Volume 35 | ) 36 | 37 | JustBeforeEach(func() { 38 | var err error 39 | firstVolume, err = client.CreateVolume(logger, "some-handle", baggageclaim.VolumeSpec{}) 40 | Expect(err).NotTo(HaveOccurred()) 41 | }) 42 | 43 | Describe("created directory", func() { 44 | var ( 45 | createdDir string 46 | ) 47 | 48 | JustBeforeEach(func() { 49 | createdDir = firstVolume.Path() 50 | }) 51 | 52 | It("is in the volume dir", func() { 53 | Expect(createdDir).To(HavePrefix(runner.VolumeDir())) 54 | }) 55 | 56 | It("creates the directory", func() { 57 | Expect(createdDir).To(BeADirectory()) 58 | }) 59 | 60 | Context("on a second request", func() { 61 | var ( 62 | secondVolume baggageclaim.Volume 63 | ) 64 | 65 | JustBeforeEach(func() { 66 | var err error 67 | secondVolume, err = client.CreateVolume(logger, "second-handle", baggageclaim.VolumeSpec{}) 68 | Expect(err).NotTo(HaveOccurred()) 69 | }) 70 | 71 | It("creates a new directory", func() { 72 | Expect(createdDir).NotTo(Equal(secondVolume.Path())) 73 | }) 74 | 75 | It("creates a new handle", func() { 76 | Expect(firstVolume.Handle).NotTo(Equal(secondVolume.Handle())) 77 | }) 78 | }) 79 | }) 80 | }) 81 | 82 | Describe("GET /volumes", func() { 83 | var ( 84 | volumes baggageclaim.Volumes 85 | ) 86 | 87 | JustBeforeEach(func() { 88 | var err error 89 | volumes, err = client.ListVolumes(logger, baggageclaim.VolumeProperties{}) 90 | Expect(err).NotTo(HaveOccurred()) 91 | }) 92 | 93 | It("returns an empty response", func() { 94 | Expect(volumes).To(BeEmpty()) 95 | }) 96 | 97 | Context("when a volume has been created", func() { 98 | var createdVolume baggageclaim.Volume 99 | 100 | BeforeEach(func() { 101 | var err error 102 | createdVolume, err = client.CreateVolume(logger, "some-handle", baggageclaim.VolumeSpec{Properties: properties}) 103 | Expect(err).NotTo(HaveOccurred()) 104 | }) 105 | 106 | It("returns it", func() { 107 | Expect(runner.CurrentHandles()).To(ConsistOf(createdVolume.Handle())) 108 | }) 109 | }) 110 | }) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /integration/baggageclaim/import_strategy_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/concourse/go-archive/tgzfs" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | 12 | "github.com/concourse/baggageclaim" 13 | ) 14 | 15 | var _ = Describe("Import Strategy", func() { 16 | var ( 17 | runner *BaggageClaimRunner 18 | client baggageclaim.Client 19 | ) 20 | 21 | BeforeEach(func() { 22 | runner = NewRunner(baggageClaimPath, "naive") 23 | runner.Start() 24 | client = runner.Client() 25 | }) 26 | 27 | AfterEach(func() { 28 | runner.Stop() 29 | runner.Cleanup() 30 | }) 31 | 32 | Describe("API", func() { 33 | Describe("POST /volumes", func() { 34 | var ( 35 | dir string 36 | 37 | strategy baggageclaim.ImportStrategy 38 | 39 | volume baggageclaim.Volume 40 | ) 41 | 42 | BeforeEach(func() { 43 | var err error 44 | dir, err = ioutil.TempDir("", "host_path") 45 | Expect(err).NotTo(HaveOccurred()) 46 | 47 | err = ioutil.WriteFile(filepath.Join(dir, "file-with-perms"), []byte("file-with-perms-contents"), 0600) 48 | Expect(err).NotTo(HaveOccurred()) 49 | 50 | err = ioutil.WriteFile(filepath.Join(dir, "some-file"), []byte("some-file-contents"), 0644) 51 | Expect(err).NotTo(HaveOccurred()) 52 | 53 | err = os.MkdirAll(filepath.Join(dir, "some-dir"), 0755) 54 | Expect(err).NotTo(HaveOccurred()) 55 | 56 | err = ioutil.WriteFile(filepath.Join(dir, "some-dir", "file-in-dir"), []byte("file-in-dir-contents"), 0644) 57 | Expect(err).NotTo(HaveOccurred()) 58 | 59 | err = os.MkdirAll(filepath.Join(dir, "empty-dir"), 0755) 60 | Expect(err).NotTo(HaveOccurred()) 61 | 62 | err = os.MkdirAll(filepath.Join(dir, "dir-with-perms"), 0700) 63 | Expect(err).NotTo(HaveOccurred()) 64 | 65 | strategy = baggageclaim.ImportStrategy{ 66 | Path: dir, 67 | } 68 | }) 69 | 70 | AfterEach(func() { 71 | Expect(os.RemoveAll(dir)).To(Succeed()) 72 | }) 73 | 74 | assert := func() { 75 | It("is in the volume dir", func() { 76 | Expect(volume.Path()).To(HavePrefix(runner.VolumeDir())) 77 | }) 78 | 79 | It("has the correct contents", func() { 80 | createdDir := volume.Path() 81 | 82 | Expect(createdDir).To(BeADirectory()) 83 | 84 | Expect(filepath.Join(createdDir, "some-file")).To(BeARegularFile()) 85 | Expect(ioutil.ReadFile(filepath.Join(createdDir, "some-file"))).To(Equal([]byte("some-file-contents"))) 86 | 87 | Expect(filepath.Join(createdDir, "file-with-perms")).To(BeARegularFile()) 88 | Expect(ioutil.ReadFile(filepath.Join(createdDir, "file-with-perms"))).To(Equal([]byte("file-with-perms-contents"))) 89 | fi, err := os.Lstat(filepath.Join(createdDir, "file-with-perms")) 90 | Expect(err).NotTo(HaveOccurred()) 91 | expectedFI, err := os.Lstat(filepath.Join(dir, "file-with-perms")) 92 | Expect(err).NotTo(HaveOccurred()) 93 | Expect(fi.Mode()).To(Equal(expectedFI.Mode())) 94 | 95 | Expect(filepath.Join(createdDir, "some-dir")).To(BeADirectory()) 96 | 97 | Expect(filepath.Join(createdDir, "some-dir", "file-in-dir")).To(BeARegularFile()) 98 | Expect(ioutil.ReadFile(filepath.Join(createdDir, "some-dir", "file-in-dir"))).To(Equal([]byte("file-in-dir-contents"))) 99 | fi, err = os.Lstat(filepath.Join(createdDir, "some-dir", "file-in-dir")) 100 | Expect(err).NotTo(HaveOccurred()) 101 | expectedFI, err = os.Lstat(filepath.Join(dir, "some-dir", "file-in-dir")) 102 | Expect(err).NotTo(HaveOccurred()) 103 | Expect(fi.Mode()).To(Equal(expectedFI.Mode())) 104 | Expect(filepath.Join(createdDir, "empty-dir")).To(BeADirectory()) 105 | 106 | Expect(filepath.Join(createdDir, "dir-with-perms")).To(BeADirectory()) 107 | fi, err = os.Lstat(filepath.Join(createdDir, "dir-with-perms")) 108 | Expect(err).NotTo(HaveOccurred()) 109 | expectedFI, err = os.Lstat(filepath.Join(dir, "dir-with-perms")) 110 | Expect(err).NotTo(HaveOccurred()) 111 | Expect(fi.Mode()).To(Equal(expectedFI.Mode())) 112 | }) 113 | } 114 | 115 | JustBeforeEach(func() { 116 | var err error 117 | volume, err = client.CreateVolume(logger, "some-handle", baggageclaim.VolumeSpec{ 118 | Strategy: strategy, 119 | }) 120 | Expect(err).NotTo(HaveOccurred()) 121 | }) 122 | 123 | assert() 124 | 125 | Context("when the path is a .tgz", func() { 126 | var tgz *os.File 127 | 128 | BeforeEach(func() { 129 | var err error 130 | tgz, err = ioutil.TempFile("", "host_path_archive") 131 | Expect(err).ToNot(HaveOccurred()) 132 | 133 | err = tgzfs.Compress(tgz, strategy.Path, ".") 134 | Expect(err).ToNot(HaveOccurred()) 135 | 136 | Expect(tgz.Close()).To(Succeed()) 137 | 138 | strategy.Path = tgz.Name() 139 | }) 140 | 141 | AfterEach(func() { 142 | Expect(os.RemoveAll(tgz.Name())).To(Succeed()) 143 | }) 144 | 145 | assert() 146 | }) 147 | }) 148 | }) 149 | }) 150 | -------------------------------------------------------------------------------- /integration/baggageclaim/integration.go: -------------------------------------------------------------------------------- 1 | package integration 2 | -------------------------------------------------------------------------------- /integration/baggageclaim/overlay_mounts_test.go: -------------------------------------------------------------------------------- 1 | //+build linux 2 | 3 | package integration_test 4 | 5 | import ( 6 | "syscall" 7 | 8 | "github.com/concourse/baggageclaim" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("baggageclaim restart", func() { 15 | 16 | var ( 17 | runner *BaggageClaimRunner 18 | client baggageclaim.Client 19 | ) 20 | 21 | BeforeEach(func() { 22 | runner = NewRunner(baggageClaimPath, "overlay") 23 | // Cannot use overlay driver if the overlays/volumes dir is fstype overlay. 24 | // This is because you can't nest overlay mounts ( a known limitation) 25 | // Mounting the TempDir as tmpfs lets us use the overlay driver for integration 26 | err := syscall.Mount("tmpfs", runner.volumeDir, "tmpfs", 0, "") 27 | Expect(err).NotTo(HaveOccurred()) 28 | 29 | runner.Start() 30 | 31 | client = runner.Client() 32 | }) 33 | 34 | AfterEach(func() { 35 | runner.Stop() 36 | 37 | err := syscall.Unmount(runner.volumeDir, 0) 38 | Expect(err).NotTo(HaveOccurred()) 39 | 40 | runner.Cleanup() 41 | }) 42 | 43 | Context("when overlay initialized volumes exist and the baggageclaim process restarts", func() { 44 | 45 | var ( 46 | createdVolume baggageclaim.Volume 47 | createdCOWVolume baggageclaim.Volume 48 | createdCOWCOWVolume baggageclaim.Volume 49 | 50 | dataInParent string 51 | err error 52 | ) 53 | 54 | BeforeEach(func() { 55 | createdVolume, err = client.CreateVolume(logger, "some-handle", baggageclaim.VolumeSpec{Strategy: baggageclaim.EmptyStrategy{}}) 56 | Expect(err).NotTo(HaveOccurred()) 57 | 58 | dataInParent = writeData(createdVolume.Path()) 59 | Expect(dataExistsInVolume(dataInParent, createdVolume.Path())).To(BeTrue()) 60 | 61 | createdCOWVolume, err = client.CreateVolume( 62 | logger, 63 | "some-cow-handle", 64 | baggageclaim.VolumeSpec{ 65 | Strategy: baggageclaim.COWStrategy{Parent: createdVolume}, 66 | Properties: map[string]string{}, 67 | Privileged: false, 68 | }, 69 | ) 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(dataExistsInVolume(dataInParent, createdCOWVolume.Path())).To(BeTrue()) 72 | 73 | Expect(runner.CurrentHandles()).To(ConsistOf( 74 | createdVolume.Handle(), 75 | createdCOWVolume.Handle(), 76 | )) 77 | 78 | createdCOWCOWVolume, err = client.CreateVolume( 79 | logger, 80 | "some-cow-cow-handle", 81 | baggageclaim.VolumeSpec{ 82 | Strategy: baggageclaim.COWStrategy{Parent: createdCOWVolume}, 83 | Properties: map[string]string{}, 84 | Privileged: false, 85 | }, 86 | ) 87 | Expect(err).NotTo(HaveOccurred()) 88 | Expect(dataExistsInVolume(dataInParent, createdCOWCOWVolume.Path())).To(BeTrue()) 89 | 90 | Expect(runner.CurrentHandles()).To(ConsistOf( 91 | createdVolume.Handle(), 92 | createdCOWVolume.Handle(), 93 | createdCOWCOWVolume.Handle(), 94 | )) 95 | 96 | err = syscall.Unmount(createdVolume.Path(), 0) 97 | Expect(err).NotTo(HaveOccurred()) 98 | err = syscall.Unmount(createdCOWVolume.Path(), 0) 99 | Expect(err).NotTo(HaveOccurred()) 100 | err = syscall.Unmount(createdCOWCOWVolume.Path(), 0) 101 | Expect(err).NotTo(HaveOccurred()) 102 | 103 | runner.Bounce() 104 | }) 105 | 106 | AfterEach(func() { 107 | err = syscall.Unmount(createdVolume.Path(), 0) 108 | Expect(err).NotTo(HaveOccurred()) 109 | err = syscall.Unmount(createdCOWVolume.Path(), 0) 110 | Expect(err).NotTo(HaveOccurred()) 111 | err = syscall.Unmount(createdCOWCOWVolume.Path(), 0) 112 | Expect(err).NotTo(HaveOccurred()) 113 | }) 114 | 115 | It("the mounts between the overlays dir and the live volumes dir should be present", func() { 116 | Expect(runner.CurrentHandles()).To(ConsistOf( 117 | createdVolume.Handle(), 118 | createdCOWVolume.Handle(), 119 | createdCOWCOWVolume.Handle(), 120 | )) 121 | 122 | Expect(dataExistsInVolume(dataInParent, createdVolume.Path())).To(BeTrue()) 123 | Expect(dataExistsInVolume(dataInParent, createdCOWVolume.Path())).To(BeTrue()) 124 | Expect(dataExistsInVolume(dataInParent, createdCOWCOWVolume.Path())).To(BeTrue()) 125 | }) 126 | }) 127 | }) 128 | -------------------------------------------------------------------------------- /integration/baggageclaim/property_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "github.com/concourse/baggageclaim" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("Properties", func() { 10 | var ( 11 | runner *BaggageClaimRunner 12 | client baggageclaim.Client 13 | ) 14 | 15 | BeforeEach(func() { 16 | runner = NewRunner(baggageClaimPath, "naive") 17 | runner.Start() 18 | 19 | client = runner.Client() 20 | }) 21 | 22 | AfterEach(func() { 23 | runner.Stop() 24 | runner.Cleanup() 25 | }) 26 | 27 | It("can manage properties", func() { 28 | emptyVolume, err := client.CreateVolume(logger, "some-handle", baggageclaim.VolumeSpec{ 29 | Properties: baggageclaim.VolumeProperties{ 30 | "property-name": "property-value", 31 | }, 32 | }) 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | err = emptyVolume.SetProperty("another-property", "another-value") 36 | Expect(err).NotTo(HaveOccurred()) 37 | 38 | someVolume, found, err := client.LookupVolume(logger, emptyVolume.Handle()) 39 | Expect(err).NotTo(HaveOccurred()) 40 | Expect(found).To(BeTrue()) 41 | 42 | Expect(someVolume.Properties()).To(Equal(baggageclaim.VolumeProperties{ 43 | "property-name": "property-value", 44 | "another-property": "another-value", 45 | })) 46 | 47 | err = someVolume.SetProperty("another-property", "yet-another-value") 48 | Expect(err).NotTo(HaveOccurred()) 49 | 50 | someVolume, found, err = client.LookupVolume(logger, someVolume.Handle()) 51 | Expect(err).NotTo(HaveOccurred()) 52 | Expect(found).To(BeTrue()) 53 | 54 | Expect(someVolume.Properties()).To(Equal(baggageclaim.VolumeProperties{ 55 | "property-name": "property-value", 56 | "another-property": "yet-another-value", 57 | })) 58 | 59 | }) 60 | 61 | It("can find a volume by its properties", func() { 62 | _, err := client.CreateVolume(logger, "some-handle-1", baggageclaim.VolumeSpec{}) 63 | Expect(err).NotTo(HaveOccurred()) 64 | 65 | emptyVolume, err := client.CreateVolume(logger, "some-handle-2", baggageclaim.VolumeSpec{ 66 | Properties: baggageclaim.VolumeProperties{ 67 | "property-name": "property-value", 68 | }, 69 | }) 70 | Expect(err).NotTo(HaveOccurred()) 71 | 72 | err = emptyVolume.SetProperty("another-property", "another-value") 73 | Expect(err).NotTo(HaveOccurred()) 74 | 75 | foundVolumes, err := client.ListVolumes(logger, baggageclaim.VolumeProperties{ 76 | "another-property": "another-value", 77 | }) 78 | Expect(err).NotTo(HaveOccurred()) 79 | 80 | Expect(foundVolumes).To(HaveLen(1)) 81 | Expect(foundVolumes[0].Properties()).To(Equal(baggageclaim.VolumeProperties{ 82 | "property-name": "property-value", 83 | "another-property": "another-value", 84 | })) 85 | }) 86 | 87 | It("returns ErrVolumeNotFound if the specified volume does not exist", func() { 88 | volume, err := client.CreateVolume(logger, "some-handle", baggageclaim.VolumeSpec{}) 89 | Expect(err).NotTo(HaveOccurred()) 90 | 91 | Expect(volume.Destroy()).To(Succeed()) 92 | 93 | err = volume.SetProperty("some", "property") 94 | Expect(err).To(Equal(baggageclaim.ErrVolumeNotFound)) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /integration/baggageclaim/restart_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/concourse/baggageclaim" 8 | ) 9 | 10 | var _ = Describe("Restarting", func() { 11 | var ( 12 | runner *BaggageClaimRunner 13 | client baggageclaim.Client 14 | ) 15 | 16 | BeforeEach(func() { 17 | runner = NewRunner(baggageClaimPath, "naive") 18 | runner.Start() 19 | 20 | client = runner.Client() 21 | }) 22 | 23 | AfterEach(func() { 24 | runner.Stop() 25 | runner.Cleanup() 26 | }) 27 | 28 | It("can get volumes after the process restarts", func() { 29 | createdVolume, err := client.CreateVolume(logger, "some-handle", baggageclaim.VolumeSpec{}) 30 | Expect(err).NotTo(HaveOccurred()) 31 | 32 | Expect(runner.CurrentHandles()).To(ConsistOf(createdVolume.Handle())) 33 | 34 | runner.Bounce() 35 | 36 | Expect(runner.CurrentHandles()).To(ConsistOf(createdVolume.Handle())) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /integration/baggageclaim/startup_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "os/exec" 5 | "strconv" 6 | "time" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | "github.com/onsi/gomega/gbytes" 11 | "github.com/onsi/gomega/gexec" 12 | ) 13 | 14 | var _ = Describe("Startup", func() { 15 | var ( 16 | process *gexec.Session 17 | ) 18 | 19 | AfterEach(func() { 20 | process.Kill().Wait(1 * time.Second) 21 | }) 22 | 23 | It("exits with an error if --volumes is not specified", func() { 24 | port := 7788 + GinkgoParallelNode() 25 | 26 | command := exec.Command( 27 | baggageClaimPath, 28 | "--bind-port", strconv.Itoa(port), 29 | ) 30 | 31 | var err error 32 | process, err = gexec.Start(command, GinkgoWriter, GinkgoWriter) 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | Eventually(process.Err).Should(gbytes.Say("the required flag `(--|/)volumes' was not specified")) 36 | Eventually(process).Should(gexec.Exit(1)) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /integration/baggageclaim/suite_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "math/rand" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strconv" 13 | "testing" 14 | "time" 15 | 16 | "code.cloudfoundry.org/lager" 17 | "code.cloudfoundry.org/lager/lagertest" 18 | "github.com/concourse/baggageclaim" 19 | "github.com/concourse/baggageclaim/client" 20 | . "github.com/onsi/ginkgo" 21 | . "github.com/onsi/gomega" 22 | "github.com/tedsuo/ifrit" 23 | "github.com/tedsuo/ifrit/ginkgomon" 24 | 25 | "github.com/onsi/gomega/gexec" 26 | ) 27 | 28 | var logger lager.Logger 29 | var baggageClaimPath string 30 | 31 | func TestIntegration(t *testing.T) { 32 | rand.Seed(time.Now().Unix()) 33 | 34 | RegisterFailHandler(Fail) 35 | RunSpecs(t, "Baggage Claim Suite") 36 | } 37 | 38 | type suiteData struct { 39 | BaggageClaimPath string 40 | } 41 | 42 | var _ = SynchronizedBeforeSuite(func() []byte { 43 | bcPath, err := gexec.Build("github.com/concourse/baggageclaim/cmd/baggageclaim") 44 | Expect(err).NotTo(HaveOccurred()) 45 | 46 | data, err := json.Marshal(suiteData{ 47 | BaggageClaimPath: bcPath, 48 | }) 49 | Expect(err).NotTo(HaveOccurred()) 50 | 51 | return data 52 | }, func(data []byte) { 53 | var suiteData suiteData 54 | err := json.Unmarshal(data, &suiteData) 55 | Expect(err).NotTo(HaveOccurred()) 56 | 57 | logger = lagertest.NewTestLogger("test") 58 | baggageClaimPath = suiteData.BaggageClaimPath 59 | 60 | // poll less frequently 61 | SetDefaultEventuallyPollingInterval(100 * time.Millisecond) 62 | SetDefaultConsistentlyPollingInterval(100 * time.Millisecond) 63 | }) 64 | 65 | var _ = SynchronizedAfterSuite(func() {}, func() { 66 | gexec.CleanupBuildArtifacts() 67 | }) 68 | 69 | type BaggageClaimRunner struct { 70 | path string 71 | process ifrit.Process 72 | port int 73 | volumeDir string 74 | driver string 75 | } 76 | 77 | func NewRunner(path string, driver string) *BaggageClaimRunner { 78 | port := 7788 + GinkgoParallelNode() 79 | 80 | volumeDir, err := ioutil.TempDir("", fmt.Sprintf("baggageclaim_volume_dir_%d", GinkgoParallelNode())) 81 | Expect(err).NotTo(HaveOccurred()) 82 | 83 | err = os.Mkdir(filepath.Join(volumeDir, "overlays"), 0700) 84 | Expect(err).NotTo(HaveOccurred()) 85 | 86 | return &BaggageClaimRunner{ 87 | path: path, 88 | port: port, 89 | volumeDir: volumeDir, 90 | driver: driver, 91 | } 92 | } 93 | 94 | func (bcr *BaggageClaimRunner) Start() { 95 | runner := ginkgomon.New(ginkgomon.Config{ 96 | Name: "baggageclaim", 97 | Command: exec.Command( 98 | bcr.path, 99 | "--bind-port", strconv.Itoa(bcr.port), 100 | "--debug-bind-port", strconv.Itoa(8099+GinkgoParallelNode()), 101 | "--volumes", bcr.volumeDir, 102 | "--driver", bcr.driver, 103 | "--overlays-dir", filepath.Join(bcr.volumeDir, "overlays"), 104 | ), 105 | StartCheck: "baggageclaim.listening", 106 | }) 107 | 108 | bcr.process = ginkgomon.Invoke(runner) 109 | } 110 | 111 | func (bcr *BaggageClaimRunner) Stop() { 112 | bcr.process.Signal(os.Kill) 113 | Eventually(bcr.process.Wait()).Should(Receive()) 114 | } 115 | 116 | func (bcr *BaggageClaimRunner) Bounce() { 117 | bcr.Stop() 118 | bcr.Start() 119 | } 120 | 121 | func (bcr *BaggageClaimRunner) Cleanup() { 122 | err := os.RemoveAll(bcr.volumeDir) 123 | Expect(err).NotTo(HaveOccurred()) 124 | } 125 | 126 | func (bcr *BaggageClaimRunner) Client() baggageclaim.Client { 127 | return client.New(fmt.Sprintf("http://localhost:%d", bcr.port), &http.Transport{DisableKeepAlives: true}) 128 | } 129 | 130 | func (bcr *BaggageClaimRunner) VolumeDir() string { 131 | return bcr.volumeDir 132 | } 133 | 134 | func (bcr *BaggageClaimRunner) Port() int { 135 | return bcr.port 136 | } 137 | 138 | func (bcr *BaggageClaimRunner) CurrentHandles() []string { 139 | volumes, err := bcr.Client().ListVolumes(logger, nil) 140 | Expect(err).NotTo(HaveOccurred()) 141 | 142 | handles := []string{} 143 | 144 | for _, v := range volumes { 145 | handles = append(handles, v.Handle()) 146 | } 147 | 148 | return handles 149 | } 150 | 151 | func writeData(volumePath string) string { 152 | filename := randSeq(10) 153 | newFilePath := filepath.Join(volumePath, filename) 154 | 155 | err := ioutil.WriteFile(newFilePath, []byte(filename), 0755) 156 | Expect(err).NotTo(HaveOccurred()) 157 | 158 | return filename 159 | } 160 | 161 | func randSeq(n int) string { 162 | letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 163 | 164 | b := make([]rune, n) 165 | for i := range b { 166 | b[i] = letters[rand.Intn(len(letters))] 167 | } 168 | return string(b) 169 | } 170 | 171 | func dataExistsInVolume(filename, volumePath string) bool { 172 | _, err := os.Stat(filepath.Join(volumePath, filename)) 173 | return err == nil 174 | } 175 | -------------------------------------------------------------------------------- /integration/fs_mounter/fs_mounter_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "runtime" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gexec" 14 | ) 15 | 16 | func mountAtPath(path string) string { 17 | diskImage := filepath.Join(path, "image.img") 18 | mountPath := filepath.Join(path, "mount") 19 | 20 | command := exec.Command( 21 | fsMounterPath, 22 | "--disk-image", diskImage, 23 | "--mount-path", mountPath, 24 | "--size-in-megabytes", "1024", 25 | ) 26 | 27 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 28 | Expect(err).NotTo(HaveOccurred()) 29 | 30 | Eventually(session, "10s").Should(gexec.Exit(0)) 31 | 32 | return mountPath 33 | } 34 | 35 | func unmountAtPath(path string) { 36 | diskImage := filepath.Join(path, "image.img") 37 | mountPath := filepath.Join(path, "mount") 38 | 39 | command := exec.Command( 40 | fsMounterPath, 41 | "--disk-image", diskImage, 42 | "--mount-path", mountPath, 43 | "--remove", 44 | ) 45 | 46 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 47 | Expect(err).NotTo(HaveOccurred()) 48 | 49 | Eventually(session, "10s").Should(gexec.Exit(0)) 50 | 51 | } 52 | 53 | var _ = Describe("FS Mounter", func() { 54 | if runtime.GOOS != "linux" { 55 | fmt.Println("\x1b[33m*** skipping btrfs tests because non-linux ***\x1b[0m") 56 | return 57 | } 58 | 59 | var ( 60 | tempDir string 61 | mountPath string 62 | ) 63 | 64 | BeforeEach(func() { 65 | var err error 66 | tempDir, err = ioutil.TempDir("", "fs_mounter_test") 67 | Expect(err).NotTo(HaveOccurred()) 68 | }) 69 | 70 | AfterEach(func() { 71 | err := os.RemoveAll(tempDir) 72 | Expect(err).NotTo(HaveOccurred()) 73 | }) 74 | 75 | Context("when starting for the first time", func() { 76 | BeforeEach(func() { 77 | mountPath = mountAtPath(tempDir) 78 | }) 79 | 80 | AfterEach(func() { 81 | unmountAtPath(tempDir) 82 | }) 83 | 84 | It("mounts a btrfs volume", func() { 85 | command := exec.Command( 86 | "btrfs", 87 | "subvolume", 88 | "show", 89 | mountPath, 90 | ) 91 | 92 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 93 | Expect(err).NotTo(HaveOccurred()) 94 | 95 | Eventually(session).Should(gexec.Exit(0)) 96 | }) 97 | }) 98 | 99 | Context("on subsequent runs", func() { 100 | BeforeEach(func() { 101 | mountPath = mountAtPath(tempDir) 102 | }) 103 | 104 | AfterEach(func() { 105 | unmountAtPath(tempDir) 106 | }) 107 | 108 | It("is idempotent", func() { 109 | path := filepath.Join(mountPath, "filez") 110 | err := ioutil.WriteFile(path, []byte("contents"), 0755) 111 | Expect(err).NotTo(HaveOccurred()) 112 | 113 | mountPath = mountAtPath(tempDir) 114 | 115 | contents, err := ioutil.ReadFile(path) 116 | Expect(err).NotTo(HaveOccurred()) 117 | 118 | Expect(string(contents)).To(Equal("contents")) 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /integration/fs_mounter/suite_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "encoding/json" 5 | "math/rand" 6 | "runtime" 7 | "testing" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | 13 | "github.com/onsi/gomega/gexec" 14 | ) 15 | 16 | var fsMounterPath string 17 | 18 | func TestIntegration(t *testing.T) { 19 | suiteName := "FS Mounter Suite" 20 | if runtime.GOOS != "linux" { 21 | suiteName = suiteName + " - skipping btrfs tests because non-linux" 22 | } 23 | 24 | rand.Seed(time.Now().Unix()) 25 | 26 | RegisterFailHandler(Fail) 27 | RunSpecs(t, suiteName) 28 | } 29 | 30 | type suiteData struct { 31 | FSMounterPath string 32 | } 33 | 34 | var _ = SynchronizedBeforeSuite(func() []byte { 35 | fsmPath, err := gexec.Build("github.com/concourse/baggageclaim/cmd/fs_mounter") 36 | Expect(err).NotTo(HaveOccurred()) 37 | 38 | data, err := json.Marshal(suiteData{ 39 | FSMounterPath: fsmPath, 40 | }) 41 | Expect(err).NotTo(HaveOccurred()) 42 | 43 | return data 44 | }, func(data []byte) { 45 | var suiteData suiteData 46 | err := json.Unmarshal(data, &suiteData) 47 | Expect(err).NotTo(HaveOccurred()) 48 | 49 | fsMounterPath = suiteData.FSMounterPath 50 | }) 51 | 52 | var _ = SynchronizedAfterSuite(func() {}, func() { 53 | gexec.CleanupBuildArtifacts() 54 | }) 55 | -------------------------------------------------------------------------------- /kernel/NOTICE: -------------------------------------------------------------------------------- 1 | Docker 2 | Copyright 2012-2016 Docker, Inc. 3 | 4 | This product includes software developed at Docker, Inc. (https://www.docker.com). 5 | 6 | This product contains software (https://github.com/kr/pty) developed 7 | by Keith Rarick, licensed under the MIT License. 8 | 9 | The following is courtesy of our legal counsel: 10 | 11 | 12 | Use and transfer of Docker may be subject to certain restrictions by the 13 | United States and other governments. 14 | It is your responsibility to ensure that your use and/or transfer does not 15 | violate applicable laws. 16 | 17 | For more information, please see https://www.bis.doc.gov 18 | 19 | See also https://www.apache.org/dev/crypto.html and/or seek legal counsel. 20 | -------------------------------------------------------------------------------- /kernel/kernel.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | // Package kernel provides helper function to get, parse and compare kernel 4 | // versions for different platforms. 5 | package kernel 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // VersionInfo holds information about the kernel. 13 | type VersionInfo struct { 14 | Kernel int // Version of the kernel (e.g. 4.1.2-generic -> 4) 15 | Major int // Major part of the kernel version (e.g. 4.1.2-generic -> 1) 16 | Minor int // Minor part of the kernel version (e.g. 4.1.2-generic -> 2) 17 | Flavor string // Flavor of the kernel version (e.g. 4.1.2-generic -> generic) 18 | } 19 | 20 | func (k *VersionInfo) String() string { 21 | return fmt.Sprintf("%d.%d.%d%s", k.Kernel, k.Major, k.Minor, k.Flavor) 22 | } 23 | 24 | // CompareKernelVersion compares two kernel.VersionInfo structs. 25 | // Returns -1 if a < b, 0 if a == b, 1 it a > b 26 | func CompareKernelVersion(a, b VersionInfo) int { 27 | if a.Kernel < b.Kernel { 28 | return -1 29 | } else if a.Kernel > b.Kernel { 30 | return 1 31 | } 32 | 33 | if a.Major < b.Major { 34 | return -1 35 | } else if a.Major > b.Major { 36 | return 1 37 | } 38 | 39 | if a.Minor < b.Minor { 40 | return -1 41 | } else if a.Minor > b.Minor { 42 | return 1 43 | } 44 | 45 | return 0 46 | } 47 | 48 | // ParseRelease parses a string and creates a VersionInfo based on it. 49 | func ParseRelease(release string) (*VersionInfo, error) { 50 | var ( 51 | kernel, major, minor, parsed int 52 | flavor, partial string 53 | ) 54 | 55 | // Ignore error from Sscanf to allow an empty flavor. Instead, just 56 | // make sure we got all the version numbers. 57 | parsed, _ = fmt.Sscanf(release, "%d.%d%s", &kernel, &major, &partial) 58 | if parsed < 2 { 59 | return nil, errors.New("Can't parse kernel version " + release) 60 | } 61 | 62 | // sometimes we have 3.12.25-gentoo, but sometimes we just have 3.12-1-amd64 63 | parsed, _ = fmt.Sscanf(partial, ".%d%s", &minor, &flavor) 64 | if parsed < 1 { 65 | flavor = partial 66 | } 67 | 68 | return &VersionInfo{ 69 | Kernel: kernel, 70 | Major: major, 71 | Minor: minor, 72 | Flavor: flavor, 73 | }, nil 74 | } 75 | -------------------------------------------------------------------------------- /kernel/kernel_unix.go: -------------------------------------------------------------------------------- 1 | // +build linux freebsd solaris openbsd 2 | 3 | // Package kernel provides helper function to get, parse and compare kernel 4 | // versions for different platforms. 5 | package kernel 6 | 7 | import "bytes" 8 | 9 | // GetKernelVersion gets the current kernel version. 10 | func GetKernelVersion() (*VersionInfo, error) { 11 | uts, err := uname() 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | release := make([]byte, len(uts.Release)) 17 | 18 | i := 0 19 | for _, c := range uts.Release { 20 | release[i] = byte(c) 21 | i++ 22 | } 23 | 24 | // Remove the \x00 from the release for Atoi to parse correctly 25 | release = release[:bytes.IndexByte(release, 0)] 26 | 27 | return ParseRelease(string(release)) 28 | } 29 | 30 | // CheckKernelVersion checks if current kernel is newer than (or equal to) 31 | // the given version. 32 | func CheckKernelVersion(k, major, minor int) (bool, error) { 33 | if v, err := GetKernelVersion(); err != nil { 34 | return false, err 35 | } else { 36 | if CompareKernelVersion(*v, VersionInfo{Kernel: k, Major: major, Minor: minor}) < 0 { 37 | return false, nil 38 | } 39 | } 40 | 41 | return true, nil 42 | } 43 | -------------------------------------------------------------------------------- /kernel/uname_linux.go: -------------------------------------------------------------------------------- 1 | package kernel 2 | 3 | import ( 4 | "syscall" 5 | ) 6 | 7 | // Utsname represents the system name structure. 8 | // It is passthrough for syscall.Utsname in order to make it portable with 9 | // other platforms where it is not available. 10 | type Utsname syscall.Utsname 11 | 12 | func uname() (*syscall.Utsname, error) { 13 | uts := &syscall.Utsname{} 14 | 15 | if err := syscall.Uname(uts); err != nil { 16 | return nil, err 17 | } 18 | return uts, nil 19 | } 20 | -------------------------------------------------------------------------------- /kernel/uname_solaris.go: -------------------------------------------------------------------------------- 1 | package kernel 2 | 3 | import ( 4 | "golang.org/x/sys/unix" 5 | ) 6 | 7 | func uname() (*unix.Utsname, error) { 8 | uts := &unix.Utsname{} 9 | 10 | if err := unix.Uname(uts); err != nil { 11 | return nil, err 12 | } 13 | return uts, nil 14 | } 15 | -------------------------------------------------------------------------------- /kernel/uname_unsupported.go: -------------------------------------------------------------------------------- 1 | // +build !linux,!solaris 2 | 3 | package kernel 4 | 5 | import ( 6 | "errors" 7 | ) 8 | 9 | // Utsname represents the system name structure. 10 | // It is defined here to make it portable as it is available on linux but not 11 | // on windows. 12 | type Utsname struct { 13 | Release [65]byte 14 | } 15 | 16 | func uname() (*Utsname, error) { 17 | return nil, errors.New("Kernel version detection is available only on linux") 18 | } 19 | -------------------------------------------------------------------------------- /resources.go: -------------------------------------------------------------------------------- 1 | package baggageclaim 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type VolumeRequest struct { 8 | Handle string `json:"handle"` 9 | Strategy *json.RawMessage `json:"strategy"` 10 | Properties VolumeProperties `json:"properties"` 11 | Privileged bool `json:"privileged,omitempty"` 12 | } 13 | 14 | type VolumeResponse struct { 15 | Handle string `json:"handle"` 16 | Path string `json:"path"` 17 | Properties VolumeProperties `json:"properties"` 18 | } 19 | 20 | type VolumeFutureResponse struct { 21 | Handle string `json:"handle"` 22 | } 23 | 24 | type PropertyRequest struct { 25 | Value string `json:"value"` 26 | } 27 | 28 | type PrivilegedRequest struct { 29 | Value bool `json:"value"` 30 | } 31 | -------------------------------------------------------------------------------- /routes.go: -------------------------------------------------------------------------------- 1 | package baggageclaim 2 | 3 | import "github.com/tedsuo/rata" 4 | 5 | const ( 6 | ListVolumes = "ListVolumes" 7 | GetVolume = "GetVolume" 8 | CreateVolume = "CreateVolume" 9 | DestroyVolume = "DestroyVolume" 10 | DestroyVolumes = "DestroyVolumes" 11 | 12 | CreateVolumeAsync = "CreateVolumeAsync" 13 | CreateVolumeAsyncCancel = "CreateVolumeAsyncCancel" 14 | CreateVolumeAsyncCheck = "CreateVolumeAsyncCheck" 15 | 16 | SetProperty = "SetProperty" 17 | GetPrivileged = "GetPrivileged" 18 | SetPrivileged = "SetPrivileged" 19 | StreamIn = "StreamIn" 20 | StreamOut = "StreamOut" 21 | StreamP2pOut = "StreamP2pOut" 22 | 23 | GetP2pUrl = "GetP2pUrl" 24 | ) 25 | 26 | var Routes = rata.Routes{ 27 | {Path: "/volumes", Method: "GET", Name: ListVolumes}, 28 | {Path: "/volumes", Method: "POST", Name: CreateVolume}, 29 | 30 | {Path: "/volumes-async", Method: "POST", Name: CreateVolumeAsync}, 31 | {Path: "/volumes-async/:handle", Method: "GET", Name: CreateVolumeAsyncCheck}, 32 | {Path: "/volumes-async/:handle", Method: "DELETE", Name: CreateVolumeAsyncCancel}, 33 | 34 | {Path: "/volumes/:handle", Method: "GET", Name: GetVolume}, 35 | {Path: "/volumes/:handle/properties/:property", Method: "PUT", Name: SetProperty}, 36 | {Path: "/volumes/:handle/privileged", Method: "GET", Name: GetPrivileged}, 37 | {Path: "/volumes/:handle/privileged", Method: "PUT", Name: SetPrivileged}, 38 | {Path: "/volumes/:handle/stream-in", Method: "PUT", Name: StreamIn}, 39 | {Path: "/volumes/:handle/stream-out", Method: "PUT", Name: StreamOut}, 40 | {Path: "/volumes/:handle/stream-p2p-out", Method: "PUT", Name: StreamP2pOut}, 41 | {Path: "/volumes/destroy", Method: "DELETE", Name: DestroyVolumes}, 42 | {Path: "/volumes/:handle", Method: "DELETE", Name: DestroyVolume}, 43 | 44 | {Path: "/p2p-url", Method: "GET", Name: GetP2pUrl}, 45 | } 46 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | not_installed() { 6 | ! command -v $1 > /dev/null 2>&1 7 | } 8 | 9 | baggageclaim_dir=$(realpath $(dirname $(dirname $0))) 10 | 11 | if not_installed ginkgo; then 12 | echo "# ginkgo is not installed! run the following command:" 13 | echo " go install github.com/onsi/ginkgo/ginkgo" 14 | exit 1 15 | fi 16 | 17 | cd $baggageclaim_dir 18 | ginkgo -r -p 19 | -------------------------------------------------------------------------------- /suite_test.go: -------------------------------------------------------------------------------- 1 | package baggageclaim_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestBaggageClaim(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "BaggageClaim Client Suite") 13 | } 14 | -------------------------------------------------------------------------------- /uidgid/mapper_linux.go: -------------------------------------------------------------------------------- 1 | package uidgid 2 | 3 | import ( 4 | "os/exec" 5 | "syscall" 6 | ) 7 | 8 | type uidGidMapper struct { 9 | uids []syscall.SysProcIDMap 10 | gids []syscall.SysProcIDMap 11 | } 12 | 13 | func NewPrivilegedMapper() Mapper { 14 | maxID := min(MustGetMaxValidUID(), MustGetMaxValidGID()) 15 | 16 | return uidGidMapper{ 17 | uids: []syscall.SysProcIDMap{ 18 | {ContainerID: maxID, HostID: 0, Size: 1}, 19 | {ContainerID: 1, HostID: 1, Size: maxID - 1}, 20 | }, 21 | gids: []syscall.SysProcIDMap{ 22 | {ContainerID: maxID, HostID: 0, Size: 1}, 23 | {ContainerID: 1, HostID: 1, Size: maxID - 1}, 24 | }, 25 | } 26 | } 27 | 28 | func NewUnprivilegedMapper() Mapper { 29 | maxID := min(MustGetMaxValidUID(), MustGetMaxValidGID()) 30 | 31 | return uidGidMapper{ 32 | uids: []syscall.SysProcIDMap{ 33 | {ContainerID: 0, HostID: maxID, Size: 1}, 34 | {ContainerID: 1, HostID: 1, Size: maxID - 1}, 35 | }, 36 | gids: []syscall.SysProcIDMap{ 37 | {ContainerID: 0, HostID: maxID, Size: 1}, 38 | {ContainerID: 1, HostID: 1, Size: maxID - 1}, 39 | }, 40 | } 41 | } 42 | 43 | func (m uidGidMapper) Apply(cmd *exec.Cmd) { 44 | cmd.SysProcAttr.Credential = &syscall.Credential{ 45 | Uid: uint32(m.uids[0].ContainerID), 46 | Gid: uint32(m.gids[0].ContainerID), 47 | } 48 | 49 | cmd.SysProcAttr.UidMappings = m.uids 50 | cmd.SysProcAttr.GidMappings = m.gids 51 | } 52 | 53 | func findMapping(idMap []syscall.SysProcIDMap, fromID int) int { 54 | for _, id := range idMap { 55 | if id.Size != 1 { 56 | continue 57 | } 58 | 59 | if id.ContainerID == fromID { 60 | return id.HostID 61 | } 62 | } 63 | 64 | return fromID 65 | } 66 | 67 | func (m uidGidMapper) Map(fromUid int, fromGid int) (int, int) { 68 | return findMapping(m.uids, fromUid), findMapping(m.gids, fromGid) 69 | } 70 | -------------------------------------------------------------------------------- /uidgid/mapper_nonlinux.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | 3 | package uidgid 4 | 5 | import ( 6 | "os/exec" 7 | ) 8 | 9 | type noopMapper struct{} 10 | 11 | func NewPrivilegedMapper() Mapper { 12 | return noopMapper{} 13 | } 14 | 15 | func NewUnprivilegedMapper() Mapper { 16 | return noopMapper{} 17 | } 18 | 19 | func (m noopMapper) Apply(cmd *exec.Cmd) {} 20 | 21 | func (m noopMapper) Map(fromUid int, fromGid int) (int, int) { 22 | return fromUid, fromGid 23 | } 24 | -------------------------------------------------------------------------------- /uidgid/max_valid_uid.go: -------------------------------------------------------------------------------- 1 | package uidgid 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | ) 9 | 10 | type IDMap string 11 | 12 | const defaultUIDMap IDMap = "/proc/self/uid_map" 13 | const defaultGIDMap IDMap = "/proc/self/gid_map" 14 | 15 | const maxInt = int(^uint(0) >> 1) 16 | 17 | func Supported() bool { 18 | return runtime.GOOS == "linux" && 19 | defaultUIDMap.Supported() && 20 | defaultGIDMap.Supported() 21 | } 22 | 23 | func MustGetMaxValidUID() int { 24 | return must(defaultUIDMap.MaxValid()) 25 | } 26 | 27 | func MustGetMaxValidGID() int { 28 | return must(defaultGIDMap.MaxValid()) 29 | } 30 | 31 | func (u IDMap) Supported() bool { 32 | _, err := os.Open(string(u)) 33 | return !os.IsNotExist(err) 34 | } 35 | 36 | func (u IDMap) MaxValid() (int, error) { 37 | f, err := os.Open(string(u)) 38 | if err != nil { 39 | return 0, err 40 | } 41 | 42 | var m uint 43 | scanner := bufio.NewScanner(f) 44 | for scanner.Scan() { 45 | var container, host, size uint 46 | if _, err := fmt.Sscanf(scanner.Text(), "%d %d %d", &container, &host, &size); err != nil { 47 | return 0, ParseError{Line: scanner.Text(), Err: err} 48 | } 49 | 50 | m = minUint(container+size-1, uint(maxInt)) 51 | } 52 | 53 | return int(m), nil 54 | } 55 | 56 | func min(a, b int) int { 57 | if a < b { 58 | return a 59 | } 60 | 61 | return b 62 | } 63 | 64 | func minUint(a, b uint) uint { 65 | if a < b { 66 | return a 67 | } 68 | 69 | return b 70 | } 71 | 72 | type ParseError struct { 73 | Line string 74 | Err error 75 | } 76 | 77 | func (p ParseError) Error() string { 78 | return fmt.Sprintf(`%s while parsing line "%s"`, p.Err, p.Line) 79 | } 80 | 81 | func must(a int, err error) int { 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | return a 87 | } 88 | -------------------------------------------------------------------------------- /uidgid/namespace.go: -------------------------------------------------------------------------------- 1 | package uidgid 2 | 3 | import ( 4 | "os/exec" 5 | "path/filepath" 6 | 7 | "code.cloudfoundry.org/lager" 8 | ) 9 | 10 | //go:generate counterfeiter . Namespacer 11 | 12 | type Namespacer interface { 13 | NamespacePath(logger lager.Logger, path string) error 14 | NamespaceCommand(cmd *exec.Cmd) 15 | } 16 | 17 | type UidNamespacer struct { 18 | Translator Translator 19 | Logger lager.Logger 20 | } 21 | 22 | func (n *UidNamespacer) NamespacePath(logger lager.Logger, rootfsPath string) error { 23 | log := logger.Session("namespace", lager.Data{ 24 | "path": rootfsPath, 25 | }) 26 | 27 | log.Debug("start") 28 | defer log.Debug("done") 29 | 30 | if err := filepath.Walk(rootfsPath, n.Translator.TranslatePath); err != nil { 31 | log.Error("failed-to-walk-and-translate", err) 32 | } 33 | 34 | return nil 35 | } 36 | 37 | func (n *UidNamespacer) NamespaceCommand(cmd *exec.Cmd) { 38 | n.Translator.TranslateCommand(cmd) 39 | } 40 | 41 | type NoopNamespacer struct{} 42 | 43 | func (NoopNamespacer) NamespacePath(lager.Logger, string) error { return nil } 44 | func (NoopNamespacer) NamespaceCommand(cmd *exec.Cmd) {} 45 | -------------------------------------------------------------------------------- /uidgid/translator.go: -------------------------------------------------------------------------------- 1 | package uidgid 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | ) 7 | 8 | //go:generate counterfeiter . Translator 9 | 10 | type Translator interface { 11 | TranslatePath(path string, info os.FileInfo, err error) error 12 | TranslateCommand(*exec.Cmd) 13 | } 14 | 15 | type translator struct { 16 | mapper Mapper 17 | chown func(path string, uid int, gid int) error 18 | chmod func(name string, mode os.FileMode) error 19 | } 20 | 21 | type Mapper interface { 22 | Map(int, int) (int, int) 23 | Apply(*exec.Cmd) 24 | } 25 | 26 | func NewTranslator(mapper Mapper) *translator { 27 | return &translator{ 28 | mapper: mapper, 29 | chown: os.Lchown, 30 | chmod: os.Chmod, 31 | } 32 | } 33 | 34 | func (t *translator) TranslatePath(path string, info os.FileInfo, err error) error { 35 | if err != nil { 36 | return err 37 | } 38 | 39 | uid, gid := t.getuidgid(info) 40 | 41 | touid, togid := t.mapper.Map(uid, gid) 42 | 43 | if touid != uid || togid != gid { 44 | mode := info.Mode() 45 | t.chown(path, touid, togid) 46 | if mode&os.ModeSymlink == 0 { 47 | t.chmod(path, mode) 48 | } 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (t *translator) TranslateCommand(cmd *exec.Cmd) { 55 | t.setuidgid(cmd) 56 | } 57 | -------------------------------------------------------------------------------- /uidgid/uidgid.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | 3 | package uidgid 4 | 5 | import ( 6 | "os" 7 | "os/exec" 8 | ) 9 | 10 | func (t *translator) getuidgid(info os.FileInfo) (int, int) { 11 | panic("not supported") 12 | } 13 | 14 | func (t *translator) setuidgid(cmd *exec.Cmd) { 15 | panic("not supported") 16 | } 17 | 18 | func newMappings(maxID int) Mapper { 19 | panic("not supported") 20 | } 21 | -------------------------------------------------------------------------------- /uidgid/uidgid_linux.go: -------------------------------------------------------------------------------- 1 | package uidgid 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "syscall" 7 | ) 8 | 9 | func (t *translator) getuidgid(info os.FileInfo) (int, int) { 10 | stat := info.Sys().(*syscall.Stat_t) 11 | return int(stat.Uid), int(stat.Gid) 12 | } 13 | 14 | func (t *translator) setuidgid(cmd *exec.Cmd) { 15 | cmd.SysProcAttr = &syscall.SysProcAttr{ 16 | Cloneflags: syscall.CLONE_NEWUSER, 17 | Credential: &syscall.Credential{ 18 | Uid: 0, 19 | Gid: 0, 20 | }, 21 | GidMappingsEnableSetgroups: true, 22 | } 23 | 24 | t.mapper.Apply(cmd) 25 | } 26 | -------------------------------------------------------------------------------- /uidgid/uidgidfakes/fake_namespacer.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package uidgidfakes 3 | 4 | import ( 5 | "os/exec" 6 | "sync" 7 | 8 | "code.cloudfoundry.org/lager" 9 | "github.com/concourse/baggageclaim/uidgid" 10 | ) 11 | 12 | type FakeNamespacer struct { 13 | NamespaceCommandStub func(*exec.Cmd) 14 | namespaceCommandMutex sync.RWMutex 15 | namespaceCommandArgsForCall []struct { 16 | arg1 *exec.Cmd 17 | } 18 | NamespacePathStub func(lager.Logger, string) error 19 | namespacePathMutex sync.RWMutex 20 | namespacePathArgsForCall []struct { 21 | arg1 lager.Logger 22 | arg2 string 23 | } 24 | namespacePathReturns struct { 25 | result1 error 26 | } 27 | namespacePathReturnsOnCall map[int]struct { 28 | result1 error 29 | } 30 | invocations map[string][][]interface{} 31 | invocationsMutex sync.RWMutex 32 | } 33 | 34 | func (fake *FakeNamespacer) NamespaceCommand(arg1 *exec.Cmd) { 35 | fake.namespaceCommandMutex.Lock() 36 | fake.namespaceCommandArgsForCall = append(fake.namespaceCommandArgsForCall, struct { 37 | arg1 *exec.Cmd 38 | }{arg1}) 39 | fake.recordInvocation("NamespaceCommand", []interface{}{arg1}) 40 | fake.namespaceCommandMutex.Unlock() 41 | if fake.NamespaceCommandStub != nil { 42 | fake.NamespaceCommandStub(arg1) 43 | } 44 | } 45 | 46 | func (fake *FakeNamespacer) NamespaceCommandCallCount() int { 47 | fake.namespaceCommandMutex.RLock() 48 | defer fake.namespaceCommandMutex.RUnlock() 49 | return len(fake.namespaceCommandArgsForCall) 50 | } 51 | 52 | func (fake *FakeNamespacer) NamespaceCommandCalls(stub func(*exec.Cmd)) { 53 | fake.namespaceCommandMutex.Lock() 54 | defer fake.namespaceCommandMutex.Unlock() 55 | fake.NamespaceCommandStub = stub 56 | } 57 | 58 | func (fake *FakeNamespacer) NamespaceCommandArgsForCall(i int) *exec.Cmd { 59 | fake.namespaceCommandMutex.RLock() 60 | defer fake.namespaceCommandMutex.RUnlock() 61 | argsForCall := fake.namespaceCommandArgsForCall[i] 62 | return argsForCall.arg1 63 | } 64 | 65 | func (fake *FakeNamespacer) NamespacePath(arg1 lager.Logger, arg2 string) error { 66 | fake.namespacePathMutex.Lock() 67 | ret, specificReturn := fake.namespacePathReturnsOnCall[len(fake.namespacePathArgsForCall)] 68 | fake.namespacePathArgsForCall = append(fake.namespacePathArgsForCall, struct { 69 | arg1 lager.Logger 70 | arg2 string 71 | }{arg1, arg2}) 72 | fake.recordInvocation("NamespacePath", []interface{}{arg1, arg2}) 73 | fake.namespacePathMutex.Unlock() 74 | if fake.NamespacePathStub != nil { 75 | return fake.NamespacePathStub(arg1, arg2) 76 | } 77 | if specificReturn { 78 | return ret.result1 79 | } 80 | fakeReturns := fake.namespacePathReturns 81 | return fakeReturns.result1 82 | } 83 | 84 | func (fake *FakeNamespacer) NamespacePathCallCount() int { 85 | fake.namespacePathMutex.RLock() 86 | defer fake.namespacePathMutex.RUnlock() 87 | return len(fake.namespacePathArgsForCall) 88 | } 89 | 90 | func (fake *FakeNamespacer) NamespacePathCalls(stub func(lager.Logger, string) error) { 91 | fake.namespacePathMutex.Lock() 92 | defer fake.namespacePathMutex.Unlock() 93 | fake.NamespacePathStub = stub 94 | } 95 | 96 | func (fake *FakeNamespacer) NamespacePathArgsForCall(i int) (lager.Logger, string) { 97 | fake.namespacePathMutex.RLock() 98 | defer fake.namespacePathMutex.RUnlock() 99 | argsForCall := fake.namespacePathArgsForCall[i] 100 | return argsForCall.arg1, argsForCall.arg2 101 | } 102 | 103 | func (fake *FakeNamespacer) NamespacePathReturns(result1 error) { 104 | fake.namespacePathMutex.Lock() 105 | defer fake.namespacePathMutex.Unlock() 106 | fake.NamespacePathStub = nil 107 | fake.namespacePathReturns = struct { 108 | result1 error 109 | }{result1} 110 | } 111 | 112 | func (fake *FakeNamespacer) NamespacePathReturnsOnCall(i int, result1 error) { 113 | fake.namespacePathMutex.Lock() 114 | defer fake.namespacePathMutex.Unlock() 115 | fake.NamespacePathStub = nil 116 | if fake.namespacePathReturnsOnCall == nil { 117 | fake.namespacePathReturnsOnCall = make(map[int]struct { 118 | result1 error 119 | }) 120 | } 121 | fake.namespacePathReturnsOnCall[i] = struct { 122 | result1 error 123 | }{result1} 124 | } 125 | 126 | func (fake *FakeNamespacer) Invocations() map[string][][]interface{} { 127 | fake.invocationsMutex.RLock() 128 | defer fake.invocationsMutex.RUnlock() 129 | fake.namespaceCommandMutex.RLock() 130 | defer fake.namespaceCommandMutex.RUnlock() 131 | fake.namespacePathMutex.RLock() 132 | defer fake.namespacePathMutex.RUnlock() 133 | copiedInvocations := map[string][][]interface{}{} 134 | for key, value := range fake.invocations { 135 | copiedInvocations[key] = value 136 | } 137 | return copiedInvocations 138 | } 139 | 140 | func (fake *FakeNamespacer) recordInvocation(key string, args []interface{}) { 141 | fake.invocationsMutex.Lock() 142 | defer fake.invocationsMutex.Unlock() 143 | if fake.invocations == nil { 144 | fake.invocations = map[string][][]interface{}{} 145 | } 146 | if fake.invocations[key] == nil { 147 | fake.invocations[key] = [][]interface{}{} 148 | } 149 | fake.invocations[key] = append(fake.invocations[key], args) 150 | } 151 | 152 | var _ uidgid.Namespacer = new(FakeNamespacer) 153 | -------------------------------------------------------------------------------- /uidgid/uidgidfakes/fake_translator.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package uidgidfakes 3 | 4 | import ( 5 | "os" 6 | "os/exec" 7 | "sync" 8 | 9 | "github.com/concourse/baggageclaim/uidgid" 10 | ) 11 | 12 | type FakeTranslator struct { 13 | TranslateCommandStub func(*exec.Cmd) 14 | translateCommandMutex sync.RWMutex 15 | translateCommandArgsForCall []struct { 16 | arg1 *exec.Cmd 17 | } 18 | TranslatePathStub func(string, os.FileInfo, error) error 19 | translatePathMutex sync.RWMutex 20 | translatePathArgsForCall []struct { 21 | arg1 string 22 | arg2 os.FileInfo 23 | arg3 error 24 | } 25 | translatePathReturns struct { 26 | result1 error 27 | } 28 | translatePathReturnsOnCall map[int]struct { 29 | result1 error 30 | } 31 | invocations map[string][][]interface{} 32 | invocationsMutex sync.RWMutex 33 | } 34 | 35 | func (fake *FakeTranslator) TranslateCommand(arg1 *exec.Cmd) { 36 | fake.translateCommandMutex.Lock() 37 | fake.translateCommandArgsForCall = append(fake.translateCommandArgsForCall, struct { 38 | arg1 *exec.Cmd 39 | }{arg1}) 40 | fake.recordInvocation("TranslateCommand", []interface{}{arg1}) 41 | fake.translateCommandMutex.Unlock() 42 | if fake.TranslateCommandStub != nil { 43 | fake.TranslateCommandStub(arg1) 44 | } 45 | } 46 | 47 | func (fake *FakeTranslator) TranslateCommandCallCount() int { 48 | fake.translateCommandMutex.RLock() 49 | defer fake.translateCommandMutex.RUnlock() 50 | return len(fake.translateCommandArgsForCall) 51 | } 52 | 53 | func (fake *FakeTranslator) TranslateCommandCalls(stub func(*exec.Cmd)) { 54 | fake.translateCommandMutex.Lock() 55 | defer fake.translateCommandMutex.Unlock() 56 | fake.TranslateCommandStub = stub 57 | } 58 | 59 | func (fake *FakeTranslator) TranslateCommandArgsForCall(i int) *exec.Cmd { 60 | fake.translateCommandMutex.RLock() 61 | defer fake.translateCommandMutex.RUnlock() 62 | argsForCall := fake.translateCommandArgsForCall[i] 63 | return argsForCall.arg1 64 | } 65 | 66 | func (fake *FakeTranslator) TranslatePath(arg1 string, arg2 os.FileInfo, arg3 error) error { 67 | fake.translatePathMutex.Lock() 68 | ret, specificReturn := fake.translatePathReturnsOnCall[len(fake.translatePathArgsForCall)] 69 | fake.translatePathArgsForCall = append(fake.translatePathArgsForCall, struct { 70 | arg1 string 71 | arg2 os.FileInfo 72 | arg3 error 73 | }{arg1, arg2, arg3}) 74 | fake.recordInvocation("TranslatePath", []interface{}{arg1, arg2, arg3}) 75 | fake.translatePathMutex.Unlock() 76 | if fake.TranslatePathStub != nil { 77 | return fake.TranslatePathStub(arg1, arg2, arg3) 78 | } 79 | if specificReturn { 80 | return ret.result1 81 | } 82 | fakeReturns := fake.translatePathReturns 83 | return fakeReturns.result1 84 | } 85 | 86 | func (fake *FakeTranslator) TranslatePathCallCount() int { 87 | fake.translatePathMutex.RLock() 88 | defer fake.translatePathMutex.RUnlock() 89 | return len(fake.translatePathArgsForCall) 90 | } 91 | 92 | func (fake *FakeTranslator) TranslatePathCalls(stub func(string, os.FileInfo, error) error) { 93 | fake.translatePathMutex.Lock() 94 | defer fake.translatePathMutex.Unlock() 95 | fake.TranslatePathStub = stub 96 | } 97 | 98 | func (fake *FakeTranslator) TranslatePathArgsForCall(i int) (string, os.FileInfo, error) { 99 | fake.translatePathMutex.RLock() 100 | defer fake.translatePathMutex.RUnlock() 101 | argsForCall := fake.translatePathArgsForCall[i] 102 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 103 | } 104 | 105 | func (fake *FakeTranslator) TranslatePathReturns(result1 error) { 106 | fake.translatePathMutex.Lock() 107 | defer fake.translatePathMutex.Unlock() 108 | fake.TranslatePathStub = nil 109 | fake.translatePathReturns = struct { 110 | result1 error 111 | }{result1} 112 | } 113 | 114 | func (fake *FakeTranslator) TranslatePathReturnsOnCall(i int, result1 error) { 115 | fake.translatePathMutex.Lock() 116 | defer fake.translatePathMutex.Unlock() 117 | fake.TranslatePathStub = nil 118 | if fake.translatePathReturnsOnCall == nil { 119 | fake.translatePathReturnsOnCall = make(map[int]struct { 120 | result1 error 121 | }) 122 | } 123 | fake.translatePathReturnsOnCall[i] = struct { 124 | result1 error 125 | }{result1} 126 | } 127 | 128 | func (fake *FakeTranslator) Invocations() map[string][][]interface{} { 129 | fake.invocationsMutex.RLock() 130 | defer fake.invocationsMutex.RUnlock() 131 | fake.translateCommandMutex.RLock() 132 | defer fake.translateCommandMutex.RUnlock() 133 | fake.translatePathMutex.RLock() 134 | defer fake.translatePathMutex.RUnlock() 135 | copiedInvocations := map[string][][]interface{}{} 136 | for key, value := range fake.invocations { 137 | copiedInvocations[key] = value 138 | } 139 | return copiedInvocations 140 | } 141 | 142 | func (fake *FakeTranslator) recordInvocation(key string, args []interface{}) { 143 | fake.invocationsMutex.Lock() 144 | defer fake.invocationsMutex.Unlock() 145 | if fake.invocations == nil { 146 | fake.invocations = map[string][][]interface{}{} 147 | } 148 | if fake.invocations[key] == nil { 149 | fake.invocations[key] = [][]interface{}{} 150 | } 151 | fake.invocations[key] = append(fake.invocations[key], args) 152 | } 153 | 154 | var _ uidgid.Translator = new(FakeTranslator) 155 | -------------------------------------------------------------------------------- /volume/copy/copy_unix.go: -------------------------------------------------------------------------------- 1 | //+build !windows 2 | 3 | package copy 4 | 5 | import ( 6 | "os/exec" 7 | ) 8 | 9 | func Cp(followSymlinks bool, src, dest string) error { 10 | cpFlags := "-a" 11 | if followSymlinks { 12 | cpFlags = "-Lr" 13 | } 14 | 15 | return exec.Command("cp", cpFlags, src+"/.", dest).Run() 16 | } 17 | -------------------------------------------------------------------------------- /volume/copy/copy_windows.go: -------------------------------------------------------------------------------- 1 | package copy 2 | 3 | import ( 4 | "os/exec" 5 | ) 6 | 7 | func Cp(followSymlinks bool, src, dest string) error { 8 | args := []string{"/e", "/nfl", "/ndl", "/mt"} 9 | if !followSymlinks { 10 | args = append(args, "/sl") 11 | } 12 | 13 | args = append(args, src, dest) 14 | 15 | return robocopy(args...) 16 | } 17 | 18 | func robocopy(args ...string) error { 19 | cmd := exec.Command("robocopy", args...) 20 | 21 | err := cmd.Run() 22 | if err != nil { 23 | // Robocopy returns a status code indicating what action occurred. 0 means nothing was copied, 24 | // 1 means that files were copied successfully. Google for additional error codes. 25 | if exitErr, ok := err.(*exec.ExitError); ok { 26 | if exitErr.ExitCode() > 1 { 27 | return err 28 | } 29 | } else { 30 | return err 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /volume/cow_strategy.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/lager" 7 | ) 8 | 9 | var ErrNoParentVolumeProvided = errors.New("no parent volume provided") 10 | var ErrParentVolumeNotFound = errors.New("parent volume not found") 11 | 12 | type COWStrategy struct { 13 | ParentHandle string 14 | } 15 | 16 | func (strategy COWStrategy) Materialize(logger lager.Logger, handle string, fs Filesystem, streamer Streamer) (FilesystemInitVolume, error) { 17 | if strategy.ParentHandle == "" { 18 | logger.Info("parent-not-specified") 19 | return nil, ErrNoParentVolumeProvided 20 | } 21 | 22 | parentVolume, found, err := fs.LookupVolume(strategy.ParentHandle) 23 | if err != nil { 24 | logger.Error("failed-to-lookup-parent", err) 25 | return nil, err 26 | } 27 | 28 | if !found { 29 | logger.Info("parent-not-found") 30 | return nil, ErrParentVolumeNotFound 31 | } 32 | 33 | return parentVolume.NewSubvolume(handle) 34 | } 35 | -------------------------------------------------------------------------------- /volume/cow_strategy_test.go: -------------------------------------------------------------------------------- 1 | package volume_test 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/lager/lagertest" 7 | . "github.com/concourse/baggageclaim/volume" 8 | "github.com/concourse/baggageclaim/volume/volumefakes" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("COWStrategy", func() { 15 | var ( 16 | strategy Strategy 17 | ) 18 | 19 | BeforeEach(func() { 20 | strategy = COWStrategy{"parent-volume"} 21 | }) 22 | 23 | Describe("Materialize", func() { 24 | var ( 25 | fakeFilesystem *volumefakes.FakeFilesystem 26 | 27 | materializedVolume FilesystemInitVolume 28 | materializeErr error 29 | ) 30 | 31 | BeforeEach(func() { 32 | fakeFilesystem = new(volumefakes.FakeFilesystem) 33 | }) 34 | 35 | JustBeforeEach(func() { 36 | materializedVolume, materializeErr = strategy.Materialize( 37 | lagertest.NewTestLogger("test"), 38 | "some-volume", 39 | fakeFilesystem, 40 | new(volumefakes.FakeStreamer), 41 | ) 42 | }) 43 | 44 | Context("when the parent volume can be found", func() { 45 | var parentVolume *volumefakes.FakeFilesystemLiveVolume 46 | 47 | BeforeEach(func() { 48 | parentVolume = new(volumefakes.FakeFilesystemLiveVolume) 49 | fakeFilesystem.LookupVolumeReturns(parentVolume, true, nil) 50 | }) 51 | 52 | Context("when creating the sub volume succeeds", func() { 53 | var fakeVolume *volumefakes.FakeFilesystemInitVolume 54 | 55 | BeforeEach(func() { 56 | parentVolume.NewSubvolumeReturns(fakeVolume, nil) 57 | }) 58 | 59 | It("succeeds", func() { 60 | Expect(materializeErr).ToNot(HaveOccurred()) 61 | }) 62 | 63 | It("returns it", func() { 64 | Expect(materializedVolume).To(Equal(fakeVolume)) 65 | }) 66 | 67 | It("created it with the correct handle", func() { 68 | handle := parentVolume.NewSubvolumeArgsForCall(0) 69 | Expect(handle).To(Equal("some-volume")) 70 | }) 71 | 72 | It("looked up the parent with the correct handle", func() { 73 | handle := fakeFilesystem.LookupVolumeArgsForCall(0) 74 | Expect(handle).To(Equal("parent-volume")) 75 | }) 76 | }) 77 | 78 | Context("when creating the sub volume fails", func() { 79 | disaster := errors.New("nope") 80 | 81 | BeforeEach(func() { 82 | parentVolume.NewSubvolumeReturns(nil, disaster) 83 | }) 84 | 85 | It("returns the error", func() { 86 | Expect(materializeErr).To(Equal(disaster)) 87 | }) 88 | }) 89 | }) 90 | 91 | Context("when no parent volume is given", func() { 92 | BeforeEach(func() { 93 | strategy = COWStrategy{""} 94 | }) 95 | 96 | It("returns ErrNoParentVolumeProvided", func() { 97 | Expect(materializeErr).To(Equal(ErrNoParentVolumeProvided)) 98 | }) 99 | 100 | It("does not look it up", func() { 101 | Expect(fakeFilesystem.LookupVolumeCallCount()).To(Equal(0)) 102 | }) 103 | }) 104 | 105 | Context("when the parent handle does not exist", func() { 106 | BeforeEach(func() { 107 | fakeFilesystem.LookupVolumeReturns(nil, false, nil) 108 | }) 109 | 110 | It("returns ErrParentVolumeNotFound", func() { 111 | Expect(materializeErr).To(Equal(ErrParentVolumeNotFound)) 112 | }) 113 | }) 114 | 115 | Context("when looking up the parent volume fails", func() { 116 | disaster := errors.New("nope") 117 | 118 | BeforeEach(func() { 119 | fakeFilesystem.LookupVolumeReturns(nil, false, disaster) 120 | }) 121 | 122 | It("returns the error", func() { 123 | Expect(materializeErr).To(Equal(disaster)) 124 | }) 125 | }) 126 | }) 127 | }) 128 | -------------------------------------------------------------------------------- /volume/driver.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | //go:generate counterfeiter . Driver 4 | 5 | type Driver interface { 6 | CreateVolume(FilesystemInitVolume) error 7 | DestroyVolume(FilesystemVolume) error 8 | 9 | CreateCopyOnWriteLayer(FilesystemInitVolume, FilesystemLiveVolume) error 10 | 11 | Recover(Filesystem) error 12 | } 13 | -------------------------------------------------------------------------------- /volume/driver/btrfs.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | 10 | "code.cloudfoundry.org/lager" 11 | "github.com/concourse/baggageclaim/volume" 12 | ) 13 | 14 | type BtrFSDriver struct { 15 | logger lager.Logger 16 | btrfsBin string 17 | } 18 | 19 | func NewBtrFSDriver( 20 | logger lager.Logger, 21 | btrfsBin string, 22 | ) *BtrFSDriver { 23 | return &BtrFSDriver{ 24 | logger: logger, 25 | btrfsBin: btrfsBin, 26 | } 27 | } 28 | 29 | func (driver *BtrFSDriver) CreateVolume(vol volume.FilesystemInitVolume) error { 30 | _, _, err := driver.run(driver.btrfsBin, "subvolume", "create", vol.DataPath()) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func (driver *BtrFSDriver) DestroyVolume(vol volume.FilesystemVolume) error { 39 | volumePathsToDelete := []string{} 40 | 41 | findSubvolumes := func(p string, f os.FileInfo, err error) error { 42 | if err != nil { 43 | return err 44 | } 45 | 46 | if !f.IsDir() { 47 | return nil 48 | } 49 | 50 | isSub, err := isSubvolume(p) 51 | if err != nil { 52 | return fmt.Errorf("failed to check if %s is a subvolume: %s", p, err) 53 | } 54 | 55 | if isSub { 56 | volumePathsToDelete = append(volumePathsToDelete, p) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | if err := filepath.Walk(vol.DataPath(), findSubvolumes); err != nil { 63 | return fmt.Errorf("recursively walking subvolumes for %s failed: %v", vol.DataPath(), err) 64 | } 65 | 66 | for i := len(volumePathsToDelete) - 1; i >= 0; i-- { 67 | _, _, err := driver.run(driver.btrfsBin, "subvolume", "delete", volumePathsToDelete[i]) 68 | if err != nil { 69 | return err 70 | } 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func (driver *BtrFSDriver) CreateCopyOnWriteLayer( 77 | childVol volume.FilesystemInitVolume, 78 | parentVol volume.FilesystemLiveVolume, 79 | ) error { 80 | _, _, err := driver.run(driver.btrfsBin, "subvolume", "snapshot", parentVol.DataPath(), childVol.DataPath()) 81 | return err 82 | } 83 | 84 | func (driver *BtrFSDriver) run(command string, args ...string) (string, string, error) { 85 | cmd := exec.Command(command, args...) 86 | 87 | logger := driver.logger.Session("run-command", lager.Data{ 88 | "command": command, 89 | "args": args, 90 | }) 91 | 92 | stdout := &bytes.Buffer{} 93 | stderr := &bytes.Buffer{} 94 | 95 | cmd.Stdout = stdout 96 | cmd.Stderr = stderr 97 | 98 | err := cmd.Run() 99 | 100 | loggerData := lager.Data{ 101 | "stdout": stdout.String(), 102 | "stderr": stderr.String(), 103 | } 104 | 105 | if err != nil { 106 | logger.Error("failed", err, loggerData) 107 | return "", "", err 108 | } 109 | 110 | logger.Debug("ran", loggerData) 111 | 112 | return stdout.String(), stderr.String(), nil 113 | } 114 | 115 | func (driver *BtrFSDriver) Recover(volume.Filesystem) error { 116 | // nothing to do 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /volume/driver/btrfs_test.go: -------------------------------------------------------------------------------- 1 | package driver_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "runtime" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gbytes" 14 | "github.com/onsi/gomega/gexec" 15 | 16 | "code.cloudfoundry.org/lager/lagertest" 17 | 18 | "github.com/concourse/baggageclaim/fs" 19 | "github.com/concourse/baggageclaim/volume" 20 | "github.com/concourse/baggageclaim/volume/driver" 21 | ) 22 | 23 | var _ = Describe("BtrFS", func() { 24 | if runtime.GOOS != "linux" { 25 | fmt.Println("\x1b[33m*** skipping btrfs tests because non-linux ***\x1b[0m") 26 | return 27 | } 28 | 29 | var ( 30 | tempDir string 31 | fsDriver *driver.BtrFSDriver 32 | filesystem *fs.BtrfsFilesystem 33 | volumeFs volume.Filesystem 34 | ) 35 | 36 | BeforeEach(func() { 37 | var err error 38 | tempDir, err = ioutil.TempDir("", "baggageclaim_driver_test") 39 | Expect(err).NotTo(HaveOccurred()) 40 | 41 | logger := lagertest.NewTestLogger("fs") 42 | 43 | imagePath := filepath.Join(tempDir, "image.img") 44 | volumesDir := filepath.Join(tempDir, "mountpoint") 45 | 46 | filesystem = fs.New(logger, imagePath, volumesDir, "mkfs.btrfs") 47 | err = filesystem.Create(1 * 1024 * 1024 * 1024) 48 | Expect(err).NotTo(HaveOccurred()) 49 | 50 | fsDriver = driver.NewBtrFSDriver(logger, "btrfs") 51 | 52 | volumeFs, err = volume.NewFilesystem(fsDriver, volumesDir) 53 | Expect(err).ToNot(HaveOccurred()) 54 | }) 55 | 56 | AfterEach(func() { 57 | err := filesystem.Delete() 58 | Expect(err).NotTo(HaveOccurred()) 59 | 60 | err = os.RemoveAll(tempDir) 61 | Expect(err).NotTo(HaveOccurred()) 62 | }) 63 | 64 | Describe("Lifecycle", func() { 65 | It("can create and delete a subvolume", func() { 66 | initVol, err := volumeFs.NewVolume("some-volume") 67 | Expect(err).NotTo(HaveOccurred()) 68 | 69 | Expect(initVol.DataPath()).To(BeADirectory()) 70 | 71 | checkSubvolume := exec.Command("btrfs", "subvolume", "show", initVol.DataPath()) 72 | session, err := gexec.Start(checkSubvolume, GinkgoWriter, GinkgoWriter) 73 | Expect(err).NotTo(HaveOccurred()) 74 | 75 | <-session.Exited 76 | Expect(session).To(gbytes.Say("some-volume")) 77 | Expect(session).To(gexec.Exit(0)) 78 | 79 | err = initVol.Destroy() 80 | Expect(err).NotTo(HaveOccurred()) 81 | 82 | Expect(initVol.DataPath()).NotTo(BeADirectory()) 83 | }) 84 | 85 | It("can delete parent volume when it has subvolumes", func() { 86 | siblingVol, err := volumeFs.NewVolume("sibling-volume") 87 | Expect(err).NotTo(HaveOccurred()) 88 | 89 | parentVol, err := volumeFs.NewVolume("parent-volume") 90 | Expect(err).NotTo(HaveOccurred()) 91 | 92 | dataPath := parentVol.DataPath() 93 | 94 | create := exec.Command("btrfs", "subvolume", "create", filepath.Join(dataPath, "sub")) 95 | session, err := gexec.Start(create, GinkgoWriter, GinkgoWriter) 96 | Expect(err).NotTo(HaveOccurred()) 97 | <-session.Exited 98 | Expect(session).To(gexec.Exit(0)) 99 | 100 | create = exec.Command("btrfs", "subvolume", "create", filepath.Join(dataPath, "sub", "sub")) 101 | session, err = gexec.Start(create, GinkgoWriter, GinkgoWriter) 102 | Expect(err).NotTo(HaveOccurred()) 103 | <-session.Exited 104 | Expect(session).To(gexec.Exit(0)) 105 | 106 | err = parentVol.Destroy() 107 | Expect(err).NotTo(HaveOccurred()) 108 | 109 | Expect(parentVol.DataPath()).ToNot(BeADirectory()) 110 | Expect(siblingVol.DataPath()).To(BeADirectory()) 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /volume/driver/driver_suite_test.go: -------------------------------------------------------------------------------- 1 | package driver_test 2 | 3 | import ( 4 | "runtime" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | 9 | "testing" 10 | ) 11 | 12 | func TestDriver(t *testing.T) { 13 | suiteName := "Driver Suite" 14 | if runtime.GOOS != "linux" { 15 | suiteName = suiteName + " - skipping btrfs tests because non-linux" 16 | } 17 | 18 | RegisterFailHandler(Fail) 19 | RunSpecs(t, suiteName) 20 | } 21 | -------------------------------------------------------------------------------- /volume/driver/is_subvolume_linux.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "syscall" 4 | 5 | const btrfsVolumeIno = 256 6 | 7 | func isSubvolume(p string) (bool, error) { 8 | var bufStat syscall.Stat_t 9 | if err := syscall.Lstat(p, &bufStat); err != nil { 10 | return false, err 11 | } 12 | 13 | return bufStat.Ino == btrfsVolumeIno, nil 14 | } 15 | -------------------------------------------------------------------------------- /volume/driver/is_subvolume_stub.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | 3 | package driver 4 | 5 | func isSubvolume(p string) (bool, error) { 6 | return false, nil 7 | } 8 | -------------------------------------------------------------------------------- /volume/driver/naive.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/concourse/baggageclaim/volume" 7 | "github.com/concourse/baggageclaim/volume/copy" 8 | ) 9 | 10 | type NaiveDriver struct{} 11 | 12 | func (driver *NaiveDriver) CreateVolume(vol volume.FilesystemInitVolume) error { 13 | return os.Mkdir(vol.DataPath(), 0755) 14 | } 15 | 16 | func (driver *NaiveDriver) DestroyVolume(vol volume.FilesystemVolume) error { 17 | return os.RemoveAll(vol.DataPath()) 18 | } 19 | 20 | func (driver *NaiveDriver) CreateCopyOnWriteLayer( 21 | childVol volume.FilesystemInitVolume, 22 | parentVol volume.FilesystemLiveVolume, 23 | ) error { 24 | return copy.Cp(false, parentVol.DataPath(), childVol.DataPath()) 25 | } 26 | 27 | func (driver *NaiveDriver) Recover(volume.Filesystem) error { 28 | // nothing to do 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /volume/driver/overlay_linux.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "syscall" 9 | 10 | "github.com/concourse/baggageclaim/volume" 11 | "github.com/concourse/baggageclaim/volume/copy" 12 | ) 13 | 14 | var mountOpts string 15 | 16 | func init() { 17 | if metacopySupported() { 18 | mountOpts = "lowerdir=%s,upperdir=%s,workdir=%s,metacopy=on" 19 | } else { 20 | mountOpts = "lowerdir=%s,upperdir=%s,workdir=%s" 21 | } 22 | } 23 | 24 | // Metacopy is an overlayfs feature. If all you're doing is chown/chmod'ing a 25 | // file then it will not create a copy of the file. Files will only be copied 26 | // when they are written to. 27 | func metacopySupported() bool { 28 | _, err := os.Stat("/sys/module/overlay/parameters/metacopy") 29 | if err != nil { 30 | return !errors.Is(err, os.ErrNotExist) 31 | } 32 | return true 33 | } 34 | 35 | type OverlayDriver struct { 36 | OverlaysDir string 37 | } 38 | 39 | func NewOverlayDriver(overlaysDir string) volume.Driver { 40 | return &OverlayDriver{ 41 | OverlaysDir: overlaysDir, 42 | } 43 | } 44 | 45 | func (driver *OverlayDriver) CreateVolume(vol volume.FilesystemInitVolume) error { 46 | path := vol.DataPath() 47 | err := os.Mkdir(path, 0755) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return driver.bindMount(vol) 53 | } 54 | 55 | func (driver *OverlayDriver) DestroyVolume(vol volume.FilesystemVolume) error { 56 | path := vol.DataPath() 57 | 58 | err := syscall.Unmount(path, 0) 59 | // when a path is already unmounted, and unmount is called 60 | // on it, syscall.EINVAL is returned as an error 61 | // ignore this error and continue to clean up 62 | if err != nil && err != os.ErrInvalid { 63 | return err 64 | } 65 | 66 | err = os.RemoveAll(driver.workDir(vol)) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | err = os.RemoveAll(driver.layerDir(vol)) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | return os.RemoveAll(path) 77 | } 78 | 79 | func (driver *OverlayDriver) CreateCopyOnWriteLayer( 80 | child volume.FilesystemInitVolume, 81 | parent volume.FilesystemLiveVolume, 82 | ) error { 83 | path := child.DataPath() 84 | err := os.MkdirAll(path, 0755) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | rootParent, err := driver.findRootParent(child, parent) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | return driver.overlayMount(child, rootParent) 95 | } 96 | 97 | func (driver *OverlayDriver) Recover(fs volume.Filesystem) error { 98 | vols, err := fs.ListVolumes() 99 | if err != nil { 100 | return err 101 | } 102 | 103 | type cow struct { 104 | parent volume.FilesystemLiveVolume 105 | child volume.FilesystemLiveVolume 106 | } 107 | 108 | cows := []cow{} 109 | for _, vol := range vols { 110 | parentVol, hasParent, err := vol.Parent() 111 | if err != nil { 112 | return fmt.Errorf("get parent: %w", err) 113 | } 114 | 115 | if hasParent { 116 | cows = append(cows, cow{ 117 | parent: parentVol, 118 | child: vol, 119 | }) 120 | continue 121 | } 122 | 123 | err = driver.bindMount(vol) 124 | if err != nil { 125 | return fmt.Errorf("recover bind mount: %w", err) 126 | } 127 | } 128 | 129 | for _, cow := range cows { 130 | rootParent, err := driver.findRootParent(cow.child, cow.parent) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | err = driver.overlayMount(cow.child, rootParent) 136 | if err != nil { 137 | return fmt.Errorf("recover overlay mount: %w", err) 138 | } 139 | } 140 | 141 | return nil 142 | } 143 | 144 | func (driver *OverlayDriver) findRootParent(child volume.FilesystemVolume, 145 | parent volume.FilesystemLiveVolume) (volume.FilesystemLiveVolume, error) { 146 | rootParent := parent 147 | grandparent, hasGrandparent, err := parent.Parent() 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | if hasGrandparent { 153 | childDir := driver.layerDir(child) 154 | parentDir := driver.layerDir(parent) 155 | err := copy.Cp(false, parentDir, childDir) 156 | if err != nil { 157 | return nil, fmt.Errorf("copy parent data to child: %w", err) 158 | } 159 | 160 | rootParent = grandparent 161 | 162 | // resolve to root volume 163 | for { 164 | grandparent, hasGrandparent, err := rootParent.Parent() 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | if !hasGrandparent { 170 | break 171 | } 172 | 173 | rootParent = grandparent 174 | } 175 | } 176 | 177 | return rootParent, nil 178 | } 179 | 180 | func (driver *OverlayDriver) bindMount(vol volume.FilesystemVolume) error { 181 | layerDir := driver.layerDir(vol) 182 | err := os.MkdirAll(layerDir, 0755) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | err = syscall.Mount(layerDir, vol.DataPath(), "", syscall.MS_BIND, "") 188 | if err != nil { 189 | return err 190 | } 191 | 192 | return nil 193 | } 194 | 195 | func (driver *OverlayDriver) overlayMount(child volume.FilesystemVolume, parent volume.FilesystemLiveVolume) error { 196 | childDir := driver.layerDir(child) 197 | err := os.MkdirAll(childDir, 0755) 198 | if err != nil { 199 | return err 200 | } 201 | 202 | workDir := driver.workDir(child) 203 | err = os.MkdirAll(workDir, 0755) 204 | if err != nil { 205 | return err 206 | } 207 | 208 | opts := fmt.Sprintf( 209 | mountOpts, 210 | parent.DataPath(), //lowerdir 211 | childDir, //upperdir 212 | workDir, //workdir 213 | ) 214 | 215 | err = syscall.Mount("overlay", child.DataPath(), "overlay", 0, opts) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | return nil 221 | } 222 | 223 | func (driver *OverlayDriver) layerDir(vol volume.FilesystemVolume) string { 224 | return filepath.Join(driver.OverlaysDir, vol.Handle()) 225 | } 226 | 227 | func (driver *OverlayDriver) workDir(vol volume.FilesystemVolume) string { 228 | return filepath.Join(driver.OverlaysDir, "work", vol.Handle()) 229 | } 230 | -------------------------------------------------------------------------------- /volume/driver/overlay_linux_test.go: -------------------------------------------------------------------------------- 1 | package driver_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/concourse/baggageclaim/volume" 10 | "github.com/concourse/baggageclaim/volume/driver" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("Overlay", func() { 17 | Describe("Driver", func() { 18 | var tmpdir string 19 | var fs volume.Filesystem 20 | 21 | BeforeEach(func() { 22 | var err error 23 | tmpdir, err = ioutil.TempDir("", "overlay-test") 24 | Expect(err).ToNot(HaveOccurred()) 25 | 26 | overlaysDir := filepath.Join(tmpdir, "overlays") 27 | overlayDriver := driver.NewOverlayDriver(overlaysDir) 28 | 29 | volumesDir := filepath.Join(tmpdir, "volumes") 30 | fs, err = volume.NewFilesystem(overlayDriver, volumesDir) 31 | Expect(err).ToNot(HaveOccurred()) 32 | }) 33 | 34 | AfterEach(func() { 35 | Expect(os.RemoveAll(tmpdir)).To(Succeed()) 36 | }) 37 | 38 | It("supports nesting >2 levels deep", func() { 39 | rootVolInit, err := fs.NewVolume("root-vol") 40 | Expect(err).ToNot(HaveOccurred()) 41 | 42 | // write to file under rootVolData 43 | rootFile := filepath.Join(rootVolInit.DataPath(), "updated-file") 44 | err = ioutil.WriteFile(rootFile, []byte("depth-0"), 0644) 45 | Expect(err).ToNot(HaveOccurred()) 46 | 47 | for depth := 1; depth <= 10; depth++ { 48 | doomedFile := filepath.Join(rootVolInit.DataPath(), fmt.Sprintf("doomed-file-%d", depth)) 49 | err := ioutil.WriteFile(doomedFile, []byte(fmt.Sprintf("i will be removed at depth %d", depth)), 0644) 50 | Expect(err).ToNot(HaveOccurred()) 51 | } 52 | 53 | rootVolLive, err := rootVolInit.Initialize() 54 | Expect(err).ToNot(HaveOccurred()) 55 | 56 | defer func() { 57 | err := rootVolLive.Destroy() 58 | Expect(err).ToNot(HaveOccurred()) 59 | }() 60 | 61 | nest := rootVolLive 62 | for depth := 1; depth <= 10; depth++ { 63 | By(fmt.Sprintf("creating a child nested %d levels deep", depth)) 64 | 65 | childInit, err := nest.NewSubvolume(fmt.Sprintf("child-vol-%d", depth)) 66 | Expect(err).ToNot(HaveOccurred()) 67 | 68 | childLive, err := childInit.Initialize() 69 | Expect(err).ToNot(HaveOccurred()) 70 | 71 | defer func() { 72 | err := childLive.Destroy() 73 | Expect(err).ToNot(HaveOccurred()) 74 | }() 75 | 76 | for i := 1; i <= 10; i++ { 77 | doomedFilePath := filepath.Join(childLive.DataPath(), fmt.Sprintf("doomed-file-%d", i)) 78 | 79 | _, statErr := os.Stat(doomedFilePath) 80 | if i < depth { 81 | Expect(statErr).To(HaveOccurred()) 82 | } else { 83 | Expect(statErr).ToNot(HaveOccurred()) 84 | 85 | if i == depth { 86 | err := os.Remove(doomedFilePath) 87 | Expect(err).ToNot(HaveOccurred()) 88 | } 89 | } 90 | } 91 | 92 | updateFilePath := filepath.Join(childLive.DataPath(), "updated-file") 93 | 94 | content, err := ioutil.ReadFile(updateFilePath) 95 | Expect(string(content)).To(Equal(fmt.Sprintf("depth-%d", depth-1))) 96 | 97 | err = ioutil.WriteFile(updateFilePath, []byte(fmt.Sprintf("depth-%d", depth)), 0644) 98 | Expect(err).ToNot(HaveOccurred()) 99 | 100 | nest = childLive 101 | } 102 | }) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /volume/empty_strategy.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import "code.cloudfoundry.org/lager" 4 | 5 | type EmptyStrategy struct{} 6 | 7 | func (EmptyStrategy) Materialize(logger lager.Logger, handle string, fs Filesystem, streamer Streamer) (FilesystemInitVolume, error) { 8 | return fs.NewVolume(handle) 9 | } 10 | -------------------------------------------------------------------------------- /volume/empty_strategy_test.go: -------------------------------------------------------------------------------- 1 | package volume_test 2 | 3 | import ( 4 | "errors" 5 | 6 | "code.cloudfoundry.org/lager/lagertest" 7 | . "github.com/concourse/baggageclaim/volume" 8 | "github.com/concourse/baggageclaim/volume/volumefakes" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("EmptyStrategy", func() { 15 | var ( 16 | strategy Strategy 17 | ) 18 | 19 | BeforeEach(func() { 20 | strategy = EmptyStrategy{} 21 | }) 22 | 23 | Describe("Materialize", func() { 24 | var ( 25 | fakeFilesystem *volumefakes.FakeFilesystem 26 | 27 | materializedVolume FilesystemInitVolume 28 | materializeErr error 29 | ) 30 | 31 | BeforeEach(func() { 32 | fakeFilesystem = new(volumefakes.FakeFilesystem) 33 | }) 34 | 35 | JustBeforeEach(func() { 36 | materializedVolume, materializeErr = strategy.Materialize( 37 | lagertest.NewTestLogger("test"), 38 | "some-volume", 39 | fakeFilesystem, 40 | new(volumefakes.FakeStreamer), 41 | ) 42 | }) 43 | 44 | Context("when creating the new volume succeeds", func() { 45 | var fakeVolume *volumefakes.FakeFilesystemInitVolume 46 | 47 | BeforeEach(func() { 48 | fakeFilesystem.NewVolumeReturns(fakeVolume, nil) 49 | }) 50 | 51 | It("succeeds", func() { 52 | Expect(materializeErr).ToNot(HaveOccurred()) 53 | }) 54 | 55 | It("returns it", func() { 56 | Expect(materializedVolume).To(Equal(fakeVolume)) 57 | }) 58 | 59 | It("created it with the correct handle", func() { 60 | handle := fakeFilesystem.NewVolumeArgsForCall(0) 61 | Expect(handle).To(Equal("some-volume")) 62 | }) 63 | }) 64 | 65 | Context("when creating the new volume fails", func() { 66 | disaster := errors.New("nope") 67 | 68 | BeforeEach(func() { 69 | fakeFilesystem.NewVolumeReturns(nil, disaster) 70 | }) 71 | 72 | It("returns the error", func() { 73 | Expect(materializeErr).To(Equal(disaster)) 74 | }) 75 | }) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /volume/filesystem.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | //go:generate counterfeiter . Filesystem 10 | 11 | type Filesystem interface { 12 | NewVolume(string) (FilesystemInitVolume, error) 13 | LookupVolume(string) (FilesystemLiveVolume, bool, error) 14 | ListVolumes() ([]FilesystemLiveVolume, error) 15 | } 16 | 17 | //go:generate counterfeiter . FilesystemVolume 18 | 19 | // FilesystemVolume represents the state of a volume's data and metadata. 20 | // 21 | // Operations will return ErrVolumeDoesNotExist if the data on disk has 22 | // disappeared. 23 | type FilesystemVolume interface { 24 | Handle() string 25 | 26 | DataPath() string 27 | 28 | LoadProperties() (Properties, error) 29 | StoreProperties(Properties) error 30 | 31 | LoadPrivileged() (bool, error) 32 | StorePrivileged(bool) error 33 | 34 | Parent() (FilesystemLiveVolume, bool, error) 35 | 36 | Destroy() error 37 | } 38 | 39 | //go:generate counterfeiter . FilesystemInitVolume 40 | 41 | type FilesystemInitVolume interface { 42 | FilesystemVolume 43 | 44 | Initialize() (FilesystemLiveVolume, error) 45 | } 46 | 47 | //go:generate counterfeiter . FilesystemLiveVolume 48 | 49 | type FilesystemLiveVolume interface { 50 | FilesystemVolume 51 | 52 | NewSubvolume(handle string) (FilesystemInitVolume, error) 53 | } 54 | 55 | const ( 56 | initDirname = "init" // volumes being initialized 57 | liveDirname = "live" // volumes accessible via API 58 | deadDirname = "dead" // volumes being torn down 59 | ) 60 | 61 | type filesystem struct { 62 | driver Driver 63 | 64 | initDir string 65 | liveDir string 66 | deadDir string 67 | } 68 | 69 | func NewFilesystem(driver Driver, parentDir string) (Filesystem, error) { 70 | initDir := filepath.Join(parentDir, initDirname) 71 | liveDir := filepath.Join(parentDir, liveDirname) 72 | deadDir := filepath.Join(parentDir, deadDirname) 73 | 74 | err := os.MkdirAll(initDir, 0755) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | err = os.MkdirAll(liveDir, 0755) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | err = os.MkdirAll(deadDir, 0755) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | return &filesystem{ 90 | driver: driver, 91 | 92 | initDir: initDir, 93 | liveDir: liveDir, 94 | deadDir: deadDir, 95 | }, nil 96 | } 97 | 98 | func (fs *filesystem) NewVolume(handle string) (FilesystemInitVolume, error) { 99 | volume, err := fs.initRawVolume(handle) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | err = fs.driver.CreateVolume(volume) 105 | if err != nil { 106 | volume.cleanup() 107 | return nil, err 108 | } 109 | 110 | return volume, nil 111 | } 112 | 113 | func (fs *filesystem) LookupVolume(handle string) (FilesystemLiveVolume, bool, error) { 114 | volumePath := fs.liveVolumePath(handle) 115 | 116 | info, err := os.Stat(volumePath) 117 | if os.IsNotExist(err) { 118 | return nil, false, nil 119 | } 120 | 121 | if err != nil { 122 | return nil, false, err 123 | } 124 | 125 | if !info.IsDir() { 126 | return nil, false, nil 127 | } 128 | 129 | return &liveVolume{ 130 | baseVolume: baseVolume{ 131 | fs: fs, 132 | 133 | handle: handle, 134 | dir: volumePath, 135 | }, 136 | }, true, nil 137 | } 138 | 139 | func (fs *filesystem) ListVolumes() ([]FilesystemLiveVolume, error) { 140 | liveDirs, err := ioutil.ReadDir(fs.liveDir) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | response := make([]FilesystemLiveVolume, 0, len(liveDirs)) 146 | 147 | for _, liveDir := range liveDirs { 148 | handle := liveDir.Name() 149 | 150 | response = append(response, &liveVolume{ 151 | baseVolume: baseVolume{ 152 | fs: fs, 153 | 154 | handle: handle, 155 | dir: fs.liveVolumePath(handle), 156 | }, 157 | }) 158 | } 159 | 160 | return response, nil 161 | } 162 | 163 | func (fs *filesystem) initRawVolume(handle string) (*initVolume, error) { 164 | volumePath := fs.initVolumePath(handle) 165 | 166 | err := os.Mkdir(volumePath, 0755) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | volume := &initVolume{ 172 | baseVolume: baseVolume{ 173 | fs: fs, 174 | 175 | handle: handle, 176 | dir: volumePath, 177 | }, 178 | } 179 | 180 | err = volume.StoreProperties(Properties{}) 181 | if err != nil { 182 | return nil, err 183 | } 184 | 185 | return volume, nil 186 | } 187 | 188 | func (fs *filesystem) initVolumePath(handle string) string { 189 | return filepath.Join(fs.initDir, handle) 190 | } 191 | 192 | func (fs *filesystem) liveVolumePath(handle string) string { 193 | return filepath.Join(fs.liveDir, handle) 194 | } 195 | 196 | func (fs *filesystem) deadVolumePath(handle string) string { 197 | return filepath.Join(fs.deadDir, handle) 198 | } 199 | 200 | type baseVolume struct { 201 | fs *filesystem 202 | 203 | handle string 204 | dir string 205 | } 206 | 207 | func (base *baseVolume) Handle() string { 208 | return base.handle 209 | } 210 | 211 | func (base *baseVolume) DataPath() string { 212 | return filepath.Join(base.dir, "volume") 213 | } 214 | 215 | func (base *baseVolume) LoadProperties() (Properties, error) { 216 | return (&Metadata{base.dir}).Properties() 217 | } 218 | 219 | func (base *baseVolume) StoreProperties(newProperties Properties) error { 220 | return (&Metadata{base.dir}).StoreProperties(newProperties) 221 | } 222 | 223 | func (base *baseVolume) LoadPrivileged() (bool, error) { 224 | return (&Metadata{base.dir}).IsPrivileged() 225 | } 226 | 227 | func (base *baseVolume) StorePrivileged(isPrivileged bool) error { 228 | return (&Metadata{base.dir}).StorePrivileged(isPrivileged) 229 | } 230 | 231 | func (base *baseVolume) Parent() (FilesystemLiveVolume, bool, error) { 232 | parentDir, err := filepath.EvalSymlinks(base.parentLink()) 233 | if os.IsNotExist(err) { 234 | return nil, false, nil 235 | } 236 | 237 | if err != nil { 238 | return nil, false, err 239 | } 240 | 241 | return &liveVolume{ 242 | baseVolume: baseVolume{ 243 | fs: base.fs, 244 | 245 | handle: filepath.Base(parentDir), 246 | dir: parentDir, 247 | }, 248 | }, true, nil 249 | } 250 | 251 | func (base *baseVolume) Destroy() error { 252 | deadDir := base.fs.deadVolumePath(base.handle) 253 | 254 | err := os.Rename(base.dir, deadDir) 255 | if err != nil { 256 | return err 257 | } 258 | 259 | deadVol := &deadVolume{ 260 | baseVolume: baseVolume{ 261 | fs: base.fs, 262 | 263 | handle: base.handle, 264 | dir: deadDir, 265 | }, 266 | } 267 | 268 | return deadVol.Destroy() 269 | } 270 | 271 | func (base *baseVolume) cleanup() error { 272 | return os.RemoveAll(base.dir) 273 | } 274 | 275 | func (base *baseVolume) parentLink() string { 276 | return filepath.Join(base.dir, "parent") 277 | } 278 | 279 | type initVolume struct { 280 | baseVolume 281 | } 282 | 283 | func (vol *initVolume) Initialize() (FilesystemLiveVolume, error) { 284 | liveDir := vol.fs.liveVolumePath(vol.handle) 285 | 286 | err := os.Rename(vol.dir, liveDir) 287 | if err != nil { 288 | return nil, err 289 | } 290 | 291 | return &liveVolume{ 292 | baseVolume: baseVolume{ 293 | fs: vol.fs, 294 | 295 | handle: vol.handle, 296 | dir: liveDir, 297 | }, 298 | }, nil 299 | } 300 | 301 | type liveVolume struct { 302 | baseVolume 303 | } 304 | 305 | func (vol *liveVolume) NewSubvolume(handle string) (FilesystemInitVolume, error) { 306 | child, err := vol.fs.initRawVolume(handle) 307 | if err != nil { 308 | return nil, err 309 | } 310 | 311 | err = vol.fs.driver.CreateCopyOnWriteLayer(child, vol) 312 | if err != nil { 313 | child.cleanup() 314 | return nil, err 315 | } 316 | 317 | err = os.Symlink(vol.dir, child.parentLink()) 318 | if err != nil { 319 | child.Destroy() 320 | return nil, err 321 | } 322 | 323 | return child, nil 324 | } 325 | 326 | type deadVolume struct { 327 | baseVolume 328 | } 329 | 330 | func (vol *deadVolume) Destroy() error { 331 | err := vol.fs.driver.DestroyVolume(vol) 332 | if err != nil { 333 | return err 334 | } 335 | 336 | return vol.cleanup() 337 | } 338 | -------------------------------------------------------------------------------- /volume/import_strategy.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "code.cloudfoundry.org/lager" 8 | "github.com/concourse/baggageclaim/volume/copy" 9 | ) 10 | 11 | type ImportStrategy struct { 12 | Path string 13 | FollowSymlinks bool 14 | } 15 | 16 | func (strategy ImportStrategy) Materialize(logger lager.Logger, handle string, fs Filesystem, streamer Streamer) (FilesystemInitVolume, error) { 17 | initVolume, err := fs.NewVolume(handle) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | destination := initVolume.DataPath() 23 | 24 | info, err := os.Stat(strategy.Path) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | if info.IsDir() { 30 | err = copy.Cp(strategy.FollowSymlinks, filepath.Clean(strategy.Path), destination) 31 | if err != nil { 32 | return nil, err 33 | } 34 | } else { 35 | tgzFile, err := os.Open(strategy.Path) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | defer tgzFile.Close() 41 | 42 | invalid, err := streamer.In(tgzFile, destination, true) 43 | if err != nil { 44 | if invalid { 45 | logger.Info("malformed-archive", lager.Data{ 46 | "error": err.Error(), 47 | }) 48 | } else { 49 | logger.Error("failed-to-stream-in", err) 50 | } 51 | 52 | return nil, err 53 | } 54 | } 55 | 56 | return initVolume, nil 57 | } 58 | -------------------------------------------------------------------------------- /volume/locker.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | //go:generate counterfeiter . LockManager 9 | 10 | type LockManager interface { 11 | Lock(key string) 12 | Unlock(key string) 13 | } 14 | 15 | type lockManager struct { 16 | locks map[string]*lockEntry 17 | mutex sync.Mutex 18 | } 19 | 20 | type lockEntry struct { 21 | ch chan struct{} 22 | count int 23 | } 24 | 25 | func NewLockManager() LockManager { 26 | locks := map[string]*lockEntry{} 27 | return &lockManager{ 28 | locks: locks, 29 | } 30 | } 31 | 32 | func (m *lockManager) Lock(key string) { 33 | m.mutex.Lock() 34 | entry, ok := m.locks[key] 35 | if !ok { 36 | entry = &lockEntry{ 37 | ch: make(chan struct{}, 1), 38 | } 39 | m.locks[key] = entry 40 | } 41 | 42 | entry.count++ 43 | m.mutex.Unlock() 44 | entry.ch <- struct{}{} 45 | } 46 | 47 | func (m *lockManager) Unlock(key string) { 48 | m.mutex.Lock() 49 | entry, ok := m.locks[key] 50 | if !ok { 51 | panic(fmt.Sprintf("key %q already unlocked", key)) 52 | } 53 | 54 | entry.count-- 55 | if entry.count == 0 { 56 | delete(m.locks, key) 57 | } 58 | 59 | m.mutex.Unlock() 60 | <-entry.ch 61 | } 62 | -------------------------------------------------------------------------------- /volume/locker_test.go: -------------------------------------------------------------------------------- 1 | package volume_test 2 | 3 | import ( 4 | "github.com/concourse/baggageclaim/volume" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("KeyedLock", func() { 10 | var lockManager volume.LockManager 11 | 12 | BeforeEach(func() { 13 | lockManager = volume.NewLockManager() 14 | }) 15 | 16 | Describe("Lock", func() { 17 | Context("when the key hasn't previously been locked", func() { 18 | It("allows access", func() { 19 | accessGrantedCh := make(chan struct{}) 20 | go func() { 21 | lockManager.Lock("the-key") 22 | close(accessGrantedCh) 23 | }() 24 | Eventually(accessGrantedCh).Should(BeClosed()) 25 | }) 26 | }) 27 | 28 | Context("when the key is currently locked", func() { 29 | It("blocks until it is unlocked", func() { 30 | firstProcReadyCh := make(chan struct{}) 31 | firstProcWaitCh := make(chan struct{}) 32 | firstProcDoneCh := make(chan struct{}) 33 | secondProcReadyCh := make(chan struct{}) 34 | secondProcDoneCh := make(chan struct{}) 35 | 36 | go func() { 37 | lockManager.Lock("the-key") 38 | close(firstProcReadyCh) 39 | <-firstProcWaitCh 40 | lockManager.Unlock("the-key") 41 | close(firstProcDoneCh) 42 | }() 43 | 44 | Eventually(firstProcReadyCh).Should(BeClosed()) 45 | 46 | go func() { 47 | lockManager.Lock("the-key") 48 | close(secondProcReadyCh) 49 | lockManager.Unlock("the-key") 50 | close(secondProcDoneCh) 51 | }() 52 | 53 | Consistently(secondProcReadyCh).ShouldNot(BeClosed()) 54 | firstProcWaitCh <- struct{}{} 55 | Eventually(secondProcDoneCh).Should(BeClosed()) 56 | }) 57 | }) 58 | }) 59 | 60 | Describe("Unlock", func() { 61 | Context("when the key has not been locked", func() { 62 | It("panics", func() { 63 | Expect(func() { 64 | lockManager.Unlock("key") 65 | }).To(Panic()) 66 | }) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /volume/metadata.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type VolumeState string 10 | 11 | const ( 12 | propertiesFileName = "properties.json" 13 | isPrivilegedFileName = "privileged.json" 14 | ) 15 | 16 | type Metadata struct { 17 | path string 18 | } 19 | 20 | // Properties File 21 | func (md *Metadata) Properties() (Properties, error) { 22 | return md.propertiesFile().Properties() 23 | } 24 | 25 | func (md *Metadata) StoreProperties(properties Properties) error { 26 | return md.propertiesFile().WriteProperties(properties) 27 | } 28 | 29 | func (md *Metadata) propertiesFile() *propertiesFile { 30 | return &propertiesFile{path: filepath.Join(md.path, propertiesFileName)} 31 | } 32 | 33 | type propertiesFile struct { 34 | path string 35 | } 36 | 37 | func (pf *propertiesFile) WriteProperties(properties Properties) error { 38 | return writeMetadataFile(pf.path, properties) 39 | } 40 | 41 | func (pf *propertiesFile) Properties() (Properties, error) { 42 | var properties Properties 43 | 44 | err := readMetadataFile(pf.path, &properties) 45 | if err != nil { 46 | return Properties{}, err 47 | } 48 | 49 | return properties, nil 50 | } 51 | 52 | func (md *Metadata) isPrivilegedFile() *isPrivilegedFile { 53 | return &isPrivilegedFile{path: filepath.Join(md.path, isPrivilegedFileName)} 54 | } 55 | 56 | func (md *Metadata) IsPrivileged() (bool, error) { 57 | return md.isPrivilegedFile().IsPrivileged() 58 | } 59 | 60 | func (md *Metadata) StorePrivileged(isPrivileged bool) error { 61 | return md.isPrivilegedFile().WritePrivileged(isPrivileged) 62 | } 63 | 64 | type isPrivilegedFile struct { 65 | path string 66 | } 67 | 68 | func (ipf *isPrivilegedFile) WritePrivileged(isPrivileged bool) error { 69 | return writeMetadataFile(ipf.path, isPrivileged) 70 | } 71 | 72 | func (ipf *isPrivilegedFile) IsPrivileged() (bool, error) { 73 | var isPrivileged bool 74 | 75 | err := readMetadataFile(ipf.path, &isPrivileged) 76 | if err != nil { 77 | return false, err 78 | } 79 | 80 | return isPrivileged, nil 81 | } 82 | 83 | func readMetadataFile(path string, properties interface{}) error { 84 | file, err := os.Open(path) 85 | if err != nil { 86 | if _, ok := err.(*os.PathError); ok { 87 | return ErrVolumeDoesNotExist 88 | } 89 | 90 | return err 91 | } 92 | defer file.Close() 93 | 94 | if err := json.NewDecoder(file).Decode(&properties); err != nil { 95 | return err 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func writeMetadataFile(path string, properties interface{}) error { 102 | file, err := os.OpenFile( 103 | path, 104 | os.O_WRONLY|os.O_CREATE, 105 | 0644, 106 | ) 107 | if err != nil { 108 | if _, ok := err.(*os.PathError); ok { 109 | return ErrVolumeDoesNotExist 110 | } 111 | 112 | return err 113 | } 114 | 115 | defer file.Close() 116 | 117 | return json.NewEncoder(file).Encode(properties) 118 | } 119 | -------------------------------------------------------------------------------- /volume/promise.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | var ErrPromiseCanceled = errors.New("promise was canceled") 9 | var ErrPromiseNotPending = errors.New("promise is not pending") 10 | var ErrPromiseStillPending = errors.New("promise is still pending") 11 | 12 | type Promise interface { 13 | IsPending() bool 14 | GetValue() (Volume, error, error) 15 | Fulfill(Volume) error 16 | Reject(error) error 17 | } 18 | 19 | type promise struct { 20 | volume *Volume 21 | err error 22 | cancel chan struct{} 23 | 24 | sync.RWMutex 25 | } 26 | 27 | func NewPromise() Promise { 28 | return &promise{ 29 | volume: nil, 30 | err: nil, 31 | cancel: make(chan struct{}), 32 | } 33 | } 34 | 35 | func (p *promise) IsPending() bool { 36 | p.RLock() 37 | defer p.RUnlock() 38 | 39 | return p.isPending() 40 | } 41 | 42 | func (p *promise) isPending() bool { 43 | return p.volume == nil && p.err == nil 44 | } 45 | 46 | func (p *promise) GetValue() (Volume, error, error) { 47 | p.RLock() 48 | defer p.RUnlock() 49 | 50 | if p.IsPending() { 51 | return Volume{}, nil, ErrPromiseStillPending 52 | } 53 | 54 | if p.volume == nil { 55 | return Volume{}, p.err, nil 56 | } 57 | return *p.volume, p.err, nil 58 | } 59 | 60 | func (p *promise) Fulfill(volume Volume) error { 61 | p.Lock() 62 | defer p.Unlock() 63 | 64 | if !p.isPending() { 65 | if p.err == ErrPromiseCanceled { 66 | return ErrPromiseCanceled 67 | } 68 | return ErrPromiseNotPending 69 | } 70 | 71 | p.volume = &volume 72 | 73 | return nil 74 | } 75 | 76 | func (p *promise) Reject(err error) error { 77 | p.Lock() 78 | defer p.Unlock() 79 | 80 | if !p.isPending() { 81 | return ErrPromiseNotPending 82 | } 83 | 84 | p.err = err 85 | 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /volume/promise_list.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | var ErrPromiseAlreadyExists = errors.New("promise already exists in list") 9 | 10 | type PromiseList interface { 11 | AddPromise(handle string, promise Promise) error 12 | GetPromise(handle string) Promise 13 | RemovePromise(handle string) 14 | } 15 | 16 | type promiseList struct { 17 | promises map[string]Promise 18 | 19 | sync.Mutex 20 | } 21 | 22 | func NewPromiseList() PromiseList { 23 | return &promiseList{ 24 | promises: make(map[string]Promise), 25 | } 26 | } 27 | 28 | func (l *promiseList) AddPromise(handle string, promise Promise) error { 29 | l.Lock() 30 | defer l.Unlock() 31 | 32 | if _, exists := l.promises[handle]; exists { 33 | return ErrPromiseAlreadyExists 34 | } 35 | 36 | l.promises[handle] = promise 37 | 38 | return nil 39 | } 40 | 41 | func (l *promiseList) GetPromise(handle string) Promise { 42 | l.Lock() 43 | defer l.Unlock() 44 | 45 | return l.promises[handle] 46 | } 47 | 48 | func (l *promiseList) RemovePromise(handle string) { 49 | l.Lock() 50 | defer l.Unlock() 51 | 52 | delete(l.promises, handle) 53 | } 54 | -------------------------------------------------------------------------------- /volume/promise_list_test.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("Volume Promise List", func() { 9 | var ( 10 | list PromiseList 11 | ) 12 | 13 | BeforeEach(func() { 14 | list = NewPromiseList() 15 | }) 16 | 17 | Context("promise doesn't exist yet", func() { 18 | It("can add promise", func() { 19 | promise := NewPromise() 20 | 21 | err := list.AddPromise("some-handle", promise) 22 | 23 | Expect(err).NotTo(HaveOccurred()) 24 | Expect(list.GetPromise("some-handle")).To(Equal(promise)) 25 | }) 26 | }) 27 | 28 | Context("promise already exists", func() { 29 | It("can't add promise again", func() { 30 | err := list.AddPromise("some-handle", NewPromise()) 31 | 32 | Expect(err).NotTo(HaveOccurred()) 33 | 34 | err = list.AddPromise("some-handle", NewPromise()) 35 | 36 | Expect(err).To(HaveOccurred()) 37 | Expect(err).To(Equal(ErrPromiseAlreadyExists)) 38 | }) 39 | 40 | It("can remove promise", func() { 41 | err := list.AddPromise("some-handle", NewPromise()) 42 | 43 | Expect(err).NotTo(HaveOccurred()) 44 | 45 | list.RemovePromise("some-handle") 46 | 47 | Expect(list.GetPromise("some-handle")).To(BeNil()) 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /volume/promise_test.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "errors" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("Volume Promise", func() { 11 | var ( 12 | promise Promise 13 | testErr = errors.New("some-error") 14 | testVolume = Volume{ 15 | Handle: "some-handle", 16 | Path: "some-path", 17 | Properties: make(Properties), 18 | } 19 | ) 20 | 21 | BeforeEach(func() { 22 | promise = NewPromise() 23 | }) 24 | 25 | Context("newly created", func() { 26 | It("is pending", func() { 27 | Expect(promise.IsPending()).To(BeTrue()) 28 | }) 29 | 30 | It("can not return a value yet", func() { 31 | _, _, err := promise.GetValue() 32 | 33 | Expect(err).To(Equal(ErrPromiseStillPending)) 34 | }) 35 | }) 36 | 37 | Context("when fulfilled", func() { 38 | It("is not pending", func() { 39 | promise.Fulfill(testVolume) 40 | 41 | Expect(promise.IsPending()).To(BeFalse()) 42 | }) 43 | 44 | It("returns a non-empty volume in value", func() { 45 | promise.Fulfill(testVolume) 46 | 47 | val, _, _ := promise.GetValue() 48 | 49 | Expect(val).To(Equal(testVolume)) 50 | }) 51 | 52 | It("returns a nil error in value", func() { 53 | promise.Fulfill(testVolume) 54 | 55 | _, val, _ := promise.GetValue() 56 | 57 | Expect(val).To(BeNil()) 58 | }) 59 | 60 | It("can return a value", func() { 61 | promise.Fulfill(testVolume) 62 | 63 | _, _, err := promise.GetValue() 64 | 65 | Expect(err).To(BeNil()) 66 | }) 67 | 68 | Context("when not pending", func() { 69 | Context("when canceled", func() { 70 | It("returns ErrPromiseCanceled", func() { 71 | promise.Reject(ErrPromiseCanceled) 72 | 73 | err := promise.Fulfill(testVolume) 74 | 75 | Expect(err).To(Equal(ErrPromiseCanceled)) 76 | }) 77 | }) 78 | 79 | Context("when fulfilled", func() { 80 | It("returns ErrPromiseNotPending", func() { 81 | promise.Fulfill(testVolume) 82 | 83 | err := promise.Fulfill(testVolume) 84 | 85 | Expect(err).To(Equal(ErrPromiseNotPending)) 86 | }) 87 | }) 88 | 89 | Context("when rejected", func() { 90 | It("returns ErrPromiseNotPending", func() { 91 | promise.Reject(testErr) 92 | 93 | err := promise.Fulfill(testVolume) 94 | 95 | Expect(err).To(Equal(ErrPromiseNotPending)) 96 | }) 97 | }) 98 | }) 99 | }) 100 | 101 | Context("when rejected", func() { 102 | It("is not pending", func() { 103 | promise.Reject(testErr) 104 | 105 | Expect(promise.IsPending()).To(BeFalse()) 106 | }) 107 | 108 | It("returns an empty volume in value", func() { 109 | promise.Reject(testErr) 110 | 111 | val, _, _ := promise.GetValue() 112 | 113 | Expect(val).To(Equal(Volume{})) 114 | }) 115 | 116 | It("returns a non-nil error in value", func() { 117 | promise.Reject(testErr) 118 | 119 | _, val, _ := promise.GetValue() 120 | 121 | Expect(val).To(Equal(testErr)) 122 | }) 123 | 124 | It("can return a value", func() { 125 | promise.Reject(testErr) 126 | 127 | _, _, err := promise.GetValue() 128 | 129 | Expect(err).To(BeNil()) 130 | }) 131 | 132 | Context("when rejecting again", func() { 133 | Context("when canceled", func() { 134 | It("returns ErrPromiseNotPending", func() { 135 | promise.Reject(ErrPromiseCanceled) 136 | 137 | err := promise.Reject(testErr) 138 | 139 | Expect(err).To(Equal(ErrPromiseNotPending)) 140 | }) 141 | }) 142 | 143 | Context("when not canceled", func() { 144 | It("returns ErrPromiseNotPending", func() { 145 | promise.Reject(testErr) 146 | 147 | err := promise.Reject(testErr) 148 | 149 | Expect(err).To(Equal(ErrPromiseNotPending)) 150 | }) 151 | }) 152 | }) 153 | }) 154 | }) 155 | -------------------------------------------------------------------------------- /volume/properties.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | type Properties map[string]string 4 | 5 | func (p Properties) HasProperties(other Properties) bool { 6 | if len(other) > len(p) { 7 | return false 8 | } 9 | 10 | for otherName, otherValue := range other { 11 | value, found := p[otherName] 12 | if !found || value != otherValue { 13 | return false 14 | } 15 | } 16 | 17 | return true 18 | } 19 | 20 | func (p Properties) UpdateProperty(name string, value string) Properties { 21 | updatedProperties := Properties{} 22 | 23 | for k, v := range p { 24 | updatedProperties[k] = v 25 | } 26 | 27 | updatedProperties[name] = value 28 | 29 | return updatedProperties 30 | } 31 | -------------------------------------------------------------------------------- /volume/properties_test.go: -------------------------------------------------------------------------------- 1 | package volume_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/concourse/baggageclaim/volume" 8 | ) 9 | 10 | var _ = Describe("Properties Superset", func() { 11 | It("return true when the two sets are equal", func() { 12 | properties := volume.Properties{ 13 | "name": "value", 14 | } 15 | 16 | result := properties.HasProperties(properties) 17 | Expect(result).To(BeTrue()) 18 | }) 19 | 20 | It("returns true if all of the elements in the query are contained in the properties", func() { 21 | properties := volume.Properties{ 22 | "name1": "value1", 23 | "name2": "value2", 24 | } 25 | 26 | query := volume.Properties{ 27 | "name1": "value1", 28 | } 29 | 30 | result := properties.HasProperties(query) 31 | Expect(result).To(BeTrue()) 32 | }) 33 | 34 | It("returns false if the query has more elements than the properties", func() { 35 | properties := volume.Properties{ 36 | "name1": "value1", 37 | } 38 | 39 | query := volume.Properties{ 40 | "name1": "value1", 41 | "name2": "value2", 42 | } 43 | 44 | result := properties.HasProperties(query) 45 | Expect(result).To(BeFalse()) 46 | }) 47 | 48 | It("returns false if all of the names in the query are not contained in the properties", func() { 49 | properties := volume.Properties{ 50 | "name1": "value1", 51 | } 52 | 53 | query := volume.Properties{ 54 | "name2": "value1", 55 | } 56 | 57 | result := properties.HasProperties(query) 58 | Expect(result).To(BeFalse()) 59 | }) 60 | 61 | It("returns false if all of the names and values in the query are not contained in the properties", func() { 62 | properties := volume.Properties{ 63 | "name1": "value1", 64 | } 65 | 66 | query := volume.Properties{ 67 | "name1": "value2", 68 | } 69 | 70 | result := properties.HasProperties(query) 71 | Expect(result).To(BeFalse()) 72 | }) 73 | 74 | It("returns false if there is a query entry that does not exist in the properties", func() { 75 | properties := volume.Properties{ 76 | "name1": "value1", 77 | "name2": "value2", 78 | } 79 | 80 | query := volume.Properties{ 81 | "name1": "value1", 82 | "name3": "value3", 83 | } 84 | 85 | result := properties.HasProperties(query) 86 | Expect(result).To(BeFalse()) 87 | }) 88 | 89 | It("returns true if the query and properties are empty", func() { 90 | properties := volume.Properties{} 91 | query := volume.Properties{} 92 | 93 | result := properties.HasProperties(query) 94 | Expect(result).To(BeTrue()) 95 | }) 96 | 97 | It("returns true if the query is empty but properties are not", func() { 98 | properties := volume.Properties{ 99 | "name1": "value1", 100 | "name2": "value2", 101 | } 102 | query := volume.Properties{} 103 | 104 | result := properties.HasProperties(query) 105 | Expect(result).To(BeTrue()) 106 | }) 107 | 108 | Describe("Update Property", func() { 109 | It("creates the property if it's not present", func() { 110 | properties := volume.Properties{} 111 | updatedProperties := properties.UpdateProperty("some", "property") 112 | 113 | Expect(updatedProperties).To(Equal(volume.Properties{"some": "property"})) 114 | }) 115 | 116 | It("does not modify the original object", func() { 117 | properties := volume.Properties{} 118 | properties.UpdateProperty("some", "property") 119 | 120 | Expect(properties).To(Equal(volume.Properties{})) 121 | }) 122 | 123 | It("updates the property if it exists already", func() { 124 | properties := volume.Properties{"some": "property"} 125 | updatedProperties := properties.UpdateProperty("some", "other-property") 126 | 127 | Expect(updatedProperties).To(Equal(volume.Properties{"some": "other-property"})) 128 | }) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /volume/strategerizer.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/concourse/baggageclaim" 9 | ) 10 | 11 | type Strategerizer interface { 12 | StrategyFor(baggageclaim.VolumeRequest) (Strategy, error) 13 | } 14 | 15 | const ( 16 | StrategyEmpty = "empty" 17 | StrategyCopyOnWrite = "cow" 18 | StrategyImport = "import" 19 | ) 20 | 21 | var ErrNoStrategy = errors.New("no strategy given") 22 | var ErrUnknownStrategy = errors.New("unknown strategy") 23 | 24 | type strategerizer struct{} 25 | 26 | func NewStrategerizer() Strategerizer { 27 | return &strategerizer{} 28 | } 29 | 30 | func (s *strategerizer) StrategyFor(request baggageclaim.VolumeRequest) (Strategy, error) { 31 | if request.Strategy == nil { 32 | return nil, ErrNoStrategy 33 | } 34 | 35 | var strategyInfo map[string]interface{} 36 | err := json.Unmarshal(*request.Strategy, &strategyInfo) 37 | if err != nil { 38 | return nil, fmt.Errorf("malformed strategy: %s", err) 39 | } 40 | 41 | strategyType, ok := strategyInfo["type"].(string) 42 | if !ok { 43 | return nil, ErrUnknownStrategy 44 | } 45 | 46 | var strategy Strategy 47 | switch strategyType { 48 | case StrategyEmpty: 49 | strategy = EmptyStrategy{} 50 | case StrategyCopyOnWrite: 51 | volume, _ := strategyInfo["volume"].(string) 52 | strategy = COWStrategy{volume} 53 | case StrategyImport: 54 | path, _ := strategyInfo["path"].(string) 55 | followSymlinks, _ := strategyInfo["follow_symlinks"].(bool) 56 | strategy = ImportStrategy{ 57 | Path: path, 58 | FollowSymlinks: followSymlinks, 59 | } 60 | default: 61 | return nil, ErrUnknownStrategy 62 | } 63 | 64 | return strategy, nil 65 | } 66 | -------------------------------------------------------------------------------- /volume/strategerizer_test.go: -------------------------------------------------------------------------------- 1 | package volume_test 2 | 3 | import ( 4 | "github.com/concourse/baggageclaim" 5 | "github.com/concourse/baggageclaim/baggageclaimfakes" 6 | "github.com/concourse/baggageclaim/volume" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("Strategerizer", func() { 13 | var ( 14 | strategerizer volume.Strategerizer 15 | ) 16 | 17 | BeforeEach(func() { 18 | strategerizer = volume.NewStrategerizer() 19 | }) 20 | 21 | Describe("StrategyFor", func() { 22 | var ( 23 | request baggageclaim.VolumeRequest 24 | 25 | strategy volume.Strategy 26 | strategyForErr error 27 | ) 28 | 29 | BeforeEach(func() { 30 | request = baggageclaim.VolumeRequest{} 31 | }) 32 | 33 | JustBeforeEach(func() { 34 | strategy, strategyForErr = strategerizer.StrategyFor(request) 35 | }) 36 | 37 | Context("with an empty strategy", func() { 38 | BeforeEach(func() { 39 | request.Strategy = baggageclaim.EmptyStrategy{}.Encode() 40 | }) 41 | 42 | It("succeeds", func() { 43 | Expect(strategyForErr).ToNot(HaveOccurred()) 44 | }) 45 | 46 | It("constructs an empty strategy", func() { 47 | Expect(strategy).To(Equal(volume.EmptyStrategy{})) 48 | }) 49 | }) 50 | 51 | Context("with an import strategy", func() { 52 | BeforeEach(func() { 53 | request.Strategy = baggageclaim.ImportStrategy{ 54 | Path: "/some/host/path", 55 | }.Encode() 56 | }) 57 | 58 | It("succeeds", func() { 59 | Expect(strategyForErr).ToNot(HaveOccurred()) 60 | }) 61 | 62 | It("constructs an import strategy", func() { 63 | Expect(strategy).To(Equal(volume.ImportStrategy{ 64 | Path: "/some/host/path", 65 | FollowSymlinks: false, 66 | })) 67 | }) 68 | 69 | Context("when follow symlinks is set", func() { 70 | BeforeEach(func() { 71 | request.Strategy = baggageclaim.ImportStrategy{ 72 | Path: "/some/host/path", 73 | FollowSymlinks: true, 74 | }.Encode() 75 | }) 76 | 77 | It("succeeds", func() { 78 | Expect(strategyForErr).ToNot(HaveOccurred()) 79 | }) 80 | 81 | It("constructs an import strategy", func() { 82 | Expect(strategy).To(Equal(volume.ImportStrategy{ 83 | Path: "/some/host/path", 84 | FollowSymlinks: true, 85 | })) 86 | }) 87 | }) 88 | }) 89 | 90 | Context("with a COW strategy", func() { 91 | BeforeEach(func() { 92 | volume := new(baggageclaimfakes.FakeVolume) 93 | volume.HandleReturns("parent-handle") 94 | request.Strategy = baggageclaim.COWStrategy{Parent: volume}.Encode() 95 | }) 96 | 97 | It("succeeds", func() { 98 | Expect(strategyForErr).ToNot(HaveOccurred()) 99 | }) 100 | 101 | It("constructs a COW strategy", func() { 102 | Expect(strategy).To(Equal(volume.COWStrategy{ParentHandle: "parent-handle"})) 103 | }) 104 | }) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /volume/strategy.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import "code.cloudfoundry.org/lager" 4 | 5 | //go:generate counterfeiter . Strategy 6 | 7 | type Strategy interface { 8 | Materialize(lager.Logger, string, Filesystem, Streamer) (FilesystemInitVolume, error) 9 | } 10 | -------------------------------------------------------------------------------- /volume/stream.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/concourse/baggageclaim/uidgid" 7 | ) 8 | 9 | //go:generate counterfeiter . Streamer 10 | 11 | type Streamer interface { 12 | In(io.Reader, string, bool) (bool, error) 13 | Out(io.Writer, string, bool) error 14 | } 15 | 16 | type tarZstdStreamer struct { 17 | namespacer uidgid.Namespacer 18 | } 19 | 20 | type tarGzipStreamer struct { 21 | namespacer uidgid.Namespacer 22 | } 23 | -------------------------------------------------------------------------------- /volume/stream_in_out_linux.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | 9 | "github.com/concourse/baggageclaim/uidgid" 10 | "github.com/klauspost/compress/zstd" 11 | ) 12 | 13 | func (streamer *tarZstdStreamer) In(tzstInput io.Reader, dest string, privileged bool) (bool, error) { 14 | tarCommand, dirFd, err := tarCmd(streamer.namespacer, privileged, dest, "-xf", "-") 15 | if err != nil { 16 | return false, err 17 | } 18 | 19 | defer dirFd.Close() 20 | 21 | zstdDecompressedStream, err := zstd.NewReader(tzstInput) 22 | if err != nil { 23 | return false, err 24 | } 25 | 26 | tarCommand.Stdin = zstdDecompressedStream 27 | tarCommand.Stdout = os.Stderr 28 | tarCommand.Stderr = os.Stderr 29 | 30 | err = tarCommand.Run() 31 | if err != nil { 32 | zstdDecompressedStream.Close() 33 | 34 | if _, ok := err.(*exec.ExitError); ok { 35 | return true, err 36 | } 37 | 38 | return false, err 39 | } 40 | 41 | zstdDecompressedStream.Close() 42 | 43 | return false, nil 44 | } 45 | 46 | func (streamer *tarZstdStreamer) Out(tzstOutput io.Writer, src string, privileged bool) error { 47 | fileInfo, err := os.Stat(src) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | var tarCommandPath, tarCommandDir string 53 | 54 | if fileInfo.IsDir() { 55 | tarCommandPath = "." 56 | tarCommandDir = src 57 | } else { 58 | tarCommandPath = filepath.Base(src) 59 | tarCommandDir = filepath.Dir(src) 60 | } 61 | 62 | tarCommand, dirFd, err := tarCmd(streamer.namespacer, privileged, tarCommandDir, "-cf", "-", tarCommandPath) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | defer dirFd.Close() 68 | 69 | zstdCompressor, err := zstd.NewWriter(tzstOutput) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | tarCommand.Stdout = zstdCompressor 75 | tarCommand.Stderr = os.Stderr 76 | 77 | err = tarCommand.Run() 78 | if err != nil { 79 | _ = zstdCompressor.Close() 80 | return err 81 | } 82 | 83 | err = zstdCompressor.Close() 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (streamer *tarGzipStreamer) In(tgzStream io.Reader, dest string, privileged bool) (bool, error) { 92 | tarCommand, dirFd, err := tarCmd(streamer.namespacer, privileged, dest, "-xz") 93 | if err != nil { 94 | return false, err 95 | } 96 | 97 | defer dirFd.Close() 98 | 99 | tarCommand.Stdin = tgzStream 100 | tarCommand.Stdout = os.Stderr 101 | tarCommand.Stderr = os.Stderr 102 | 103 | err = tarCommand.Run() 104 | if err != nil { 105 | if _, ok := err.(*exec.ExitError); ok { 106 | return true, err 107 | } 108 | 109 | return false, err 110 | } 111 | 112 | return false, nil 113 | } 114 | 115 | func (streamer *tarGzipStreamer) Out(w io.Writer, src string, privileged bool) error { 116 | fileInfo, err := os.Stat(src) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | var tarCommandPath, tarCommandDir string 122 | 123 | if fileInfo.IsDir() { 124 | tarCommandPath = "." 125 | tarCommandDir = src 126 | } else { 127 | tarCommandPath = filepath.Base(src) 128 | tarCommandDir = filepath.Dir(src) 129 | } 130 | 131 | tarCommand, dirFd, err := tarCmd(streamer.namespacer, privileged, tarCommandDir, "-cz", tarCommandPath) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | defer dirFd.Close() 137 | 138 | tarCommand.Stdout = w 139 | tarCommand.Stderr = os.Stderr 140 | 141 | err = tarCommand.Run() 142 | if err != nil { 143 | return err 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func tarCmd(namespacer uidgid.Namespacer, privileged bool, dir string, args ...string) (*exec.Cmd, *os.File, error) { 150 | // 'tar' may run as MAX_UID in order to remap UIDs when streaming into an 151 | // unprivileged volume. this may cause permission issues when exec'ing as it 152 | // may not be able to even see the destination directory as non-root. 153 | // 154 | // so, open the directory while we're root, and pass it as a fd to the 155 | // process. 156 | dirFd, err := os.Open(dir) 157 | if err != nil { 158 | return nil, nil, err 159 | } 160 | 161 | tarCommand := exec.Command("tar", append([]string{"-C", "/dev/fd/3"}, args...)...) 162 | tarCommand.ExtraFiles = []*os.File{dirFd} 163 | 164 | if !privileged { 165 | namespacer.NamespaceCommand(tarCommand) 166 | } 167 | 168 | return tarCommand, dirFd, nil 169 | } 170 | -------------------------------------------------------------------------------- /volume/stream_in_out_nonlinux.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | 3 | package volume 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/concourse/go-archive/tarfs" 11 | "github.com/concourse/go-archive/tgzfs" 12 | "github.com/klauspost/compress/zstd" 13 | ) 14 | 15 | func (streamer *tarGzipStreamer) In(stream io.Reader, dest string, privileged bool) (bool, error) { 16 | err := tgzfs.Extract(stream, dest) 17 | if err != nil { 18 | return true, err 19 | } 20 | 21 | return false, nil 22 | } 23 | 24 | func (streamer *tarGzipStreamer) Out(w io.Writer, src string, privileged bool) error { 25 | fileInfo, err := os.Stat(src) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | var tarDir, tarPath string 31 | 32 | if fileInfo.IsDir() { 33 | tarDir = src 34 | tarPath = "." 35 | } else { 36 | tarDir = filepath.Dir(src) 37 | tarPath = filepath.Base(src) 38 | } 39 | 40 | return tgzfs.Compress(w, tarDir, tarPath) 41 | } 42 | 43 | func (streamer *tarZstdStreamer) In(stream io.Reader, dest string, privileged bool) (bool, error) { 44 | zstdStreamReader, err := zstd.NewReader(stream) 45 | if err != nil { 46 | return true, err 47 | } 48 | 49 | err = tarfs.Extract(zstdStreamReader, dest) 50 | if err != nil { 51 | zstdStreamReader.Close() 52 | return true, err 53 | } 54 | 55 | zstdStreamReader.Close() 56 | 57 | return false, nil 58 | } 59 | 60 | func (streamer *tarZstdStreamer) Out(w io.Writer, src string, privileged bool) error { 61 | fileInfo, err := os.Stat(src) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | var tarDir, tarPath string 67 | 68 | if fileInfo.IsDir() { 69 | tarDir = src 70 | tarPath = "." 71 | } else { 72 | tarDir = filepath.Dir(src) 73 | tarPath = filepath.Base(src) 74 | } 75 | 76 | zstdStreamWriter, err := zstd.NewWriter(w) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | err = tarfs.Compress(zstdStreamWriter, tarDir, tarPath) 82 | if err != nil { 83 | zstdStreamWriter.Close() 84 | return err 85 | } 86 | 87 | err = zstdStreamWriter.Close() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /volume/volume.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | type Volume struct { 4 | Handle string `json:"handle"` 5 | Path string `json:"path"` 6 | Properties Properties `json:"properties"` 7 | Privileged bool `json:"privileged"` 8 | } 9 | 10 | type Volumes []Volume 11 | -------------------------------------------------------------------------------- /volume/volume_suite_test.go: -------------------------------------------------------------------------------- 1 | package volume_test 2 | 3 | import ( 4 | "runtime" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | 9 | "testing" 10 | ) 11 | 12 | func TestVolume(t *testing.T) { 13 | suiteName := "Volume Suite" 14 | if runtime.GOOS != "linux" { 15 | suiteName = suiteName + " - skipping btrfs tests because non-linux" 16 | } 17 | 18 | RegisterFailHandler(Fail) 19 | RunSpecs(t, suiteName) 20 | } 21 | -------------------------------------------------------------------------------- /volume/volumefakes/fake_filesystem.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package volumefakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/concourse/baggageclaim/volume" 8 | ) 9 | 10 | type FakeFilesystem struct { 11 | ListVolumesStub func() ([]volume.FilesystemLiveVolume, error) 12 | listVolumesMutex sync.RWMutex 13 | listVolumesArgsForCall []struct { 14 | } 15 | listVolumesReturns struct { 16 | result1 []volume.FilesystemLiveVolume 17 | result2 error 18 | } 19 | listVolumesReturnsOnCall map[int]struct { 20 | result1 []volume.FilesystemLiveVolume 21 | result2 error 22 | } 23 | LookupVolumeStub func(string) (volume.FilesystemLiveVolume, bool, error) 24 | lookupVolumeMutex sync.RWMutex 25 | lookupVolumeArgsForCall []struct { 26 | arg1 string 27 | } 28 | lookupVolumeReturns struct { 29 | result1 volume.FilesystemLiveVolume 30 | result2 bool 31 | result3 error 32 | } 33 | lookupVolumeReturnsOnCall map[int]struct { 34 | result1 volume.FilesystemLiveVolume 35 | result2 bool 36 | result3 error 37 | } 38 | NewVolumeStub func(string) (volume.FilesystemInitVolume, error) 39 | newVolumeMutex sync.RWMutex 40 | newVolumeArgsForCall []struct { 41 | arg1 string 42 | } 43 | newVolumeReturns struct { 44 | result1 volume.FilesystemInitVolume 45 | result2 error 46 | } 47 | newVolumeReturnsOnCall map[int]struct { 48 | result1 volume.FilesystemInitVolume 49 | result2 error 50 | } 51 | invocations map[string][][]interface{} 52 | invocationsMutex sync.RWMutex 53 | } 54 | 55 | func (fake *FakeFilesystem) ListVolumes() ([]volume.FilesystemLiveVolume, error) { 56 | fake.listVolumesMutex.Lock() 57 | ret, specificReturn := fake.listVolumesReturnsOnCall[len(fake.listVolumesArgsForCall)] 58 | fake.listVolumesArgsForCall = append(fake.listVolumesArgsForCall, struct { 59 | }{}) 60 | fake.recordInvocation("ListVolumes", []interface{}{}) 61 | fake.listVolumesMutex.Unlock() 62 | if fake.ListVolumesStub != nil { 63 | return fake.ListVolumesStub() 64 | } 65 | if specificReturn { 66 | return ret.result1, ret.result2 67 | } 68 | fakeReturns := fake.listVolumesReturns 69 | return fakeReturns.result1, fakeReturns.result2 70 | } 71 | 72 | func (fake *FakeFilesystem) ListVolumesCallCount() int { 73 | fake.listVolumesMutex.RLock() 74 | defer fake.listVolumesMutex.RUnlock() 75 | return len(fake.listVolumesArgsForCall) 76 | } 77 | 78 | func (fake *FakeFilesystem) ListVolumesCalls(stub func() ([]volume.FilesystemLiveVolume, error)) { 79 | fake.listVolumesMutex.Lock() 80 | defer fake.listVolumesMutex.Unlock() 81 | fake.ListVolumesStub = stub 82 | } 83 | 84 | func (fake *FakeFilesystem) ListVolumesReturns(result1 []volume.FilesystemLiveVolume, result2 error) { 85 | fake.listVolumesMutex.Lock() 86 | defer fake.listVolumesMutex.Unlock() 87 | fake.ListVolumesStub = nil 88 | fake.listVolumesReturns = struct { 89 | result1 []volume.FilesystemLiveVolume 90 | result2 error 91 | }{result1, result2} 92 | } 93 | 94 | func (fake *FakeFilesystem) ListVolumesReturnsOnCall(i int, result1 []volume.FilesystemLiveVolume, result2 error) { 95 | fake.listVolumesMutex.Lock() 96 | defer fake.listVolumesMutex.Unlock() 97 | fake.ListVolumesStub = nil 98 | if fake.listVolumesReturnsOnCall == nil { 99 | fake.listVolumesReturnsOnCall = make(map[int]struct { 100 | result1 []volume.FilesystemLiveVolume 101 | result2 error 102 | }) 103 | } 104 | fake.listVolumesReturnsOnCall[i] = struct { 105 | result1 []volume.FilesystemLiveVolume 106 | result2 error 107 | }{result1, result2} 108 | } 109 | 110 | func (fake *FakeFilesystem) LookupVolume(arg1 string) (volume.FilesystemLiveVolume, bool, error) { 111 | fake.lookupVolumeMutex.Lock() 112 | ret, specificReturn := fake.lookupVolumeReturnsOnCall[len(fake.lookupVolumeArgsForCall)] 113 | fake.lookupVolumeArgsForCall = append(fake.lookupVolumeArgsForCall, struct { 114 | arg1 string 115 | }{arg1}) 116 | fake.recordInvocation("LookupVolume", []interface{}{arg1}) 117 | fake.lookupVolumeMutex.Unlock() 118 | if fake.LookupVolumeStub != nil { 119 | return fake.LookupVolumeStub(arg1) 120 | } 121 | if specificReturn { 122 | return ret.result1, ret.result2, ret.result3 123 | } 124 | fakeReturns := fake.lookupVolumeReturns 125 | return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 126 | } 127 | 128 | func (fake *FakeFilesystem) LookupVolumeCallCount() int { 129 | fake.lookupVolumeMutex.RLock() 130 | defer fake.lookupVolumeMutex.RUnlock() 131 | return len(fake.lookupVolumeArgsForCall) 132 | } 133 | 134 | func (fake *FakeFilesystem) LookupVolumeCalls(stub func(string) (volume.FilesystemLiveVolume, bool, error)) { 135 | fake.lookupVolumeMutex.Lock() 136 | defer fake.lookupVolumeMutex.Unlock() 137 | fake.LookupVolumeStub = stub 138 | } 139 | 140 | func (fake *FakeFilesystem) LookupVolumeArgsForCall(i int) string { 141 | fake.lookupVolumeMutex.RLock() 142 | defer fake.lookupVolumeMutex.RUnlock() 143 | argsForCall := fake.lookupVolumeArgsForCall[i] 144 | return argsForCall.arg1 145 | } 146 | 147 | func (fake *FakeFilesystem) LookupVolumeReturns(result1 volume.FilesystemLiveVolume, result2 bool, result3 error) { 148 | fake.lookupVolumeMutex.Lock() 149 | defer fake.lookupVolumeMutex.Unlock() 150 | fake.LookupVolumeStub = nil 151 | fake.lookupVolumeReturns = struct { 152 | result1 volume.FilesystemLiveVolume 153 | result2 bool 154 | result3 error 155 | }{result1, result2, result3} 156 | } 157 | 158 | func (fake *FakeFilesystem) LookupVolumeReturnsOnCall(i int, result1 volume.FilesystemLiveVolume, result2 bool, result3 error) { 159 | fake.lookupVolumeMutex.Lock() 160 | defer fake.lookupVolumeMutex.Unlock() 161 | fake.LookupVolumeStub = nil 162 | if fake.lookupVolumeReturnsOnCall == nil { 163 | fake.lookupVolumeReturnsOnCall = make(map[int]struct { 164 | result1 volume.FilesystemLiveVolume 165 | result2 bool 166 | result3 error 167 | }) 168 | } 169 | fake.lookupVolumeReturnsOnCall[i] = struct { 170 | result1 volume.FilesystemLiveVolume 171 | result2 bool 172 | result3 error 173 | }{result1, result2, result3} 174 | } 175 | 176 | func (fake *FakeFilesystem) NewVolume(arg1 string) (volume.FilesystemInitVolume, error) { 177 | fake.newVolumeMutex.Lock() 178 | ret, specificReturn := fake.newVolumeReturnsOnCall[len(fake.newVolumeArgsForCall)] 179 | fake.newVolumeArgsForCall = append(fake.newVolumeArgsForCall, struct { 180 | arg1 string 181 | }{arg1}) 182 | fake.recordInvocation("NewVolume", []interface{}{arg1}) 183 | fake.newVolumeMutex.Unlock() 184 | if fake.NewVolumeStub != nil { 185 | return fake.NewVolumeStub(arg1) 186 | } 187 | if specificReturn { 188 | return ret.result1, ret.result2 189 | } 190 | fakeReturns := fake.newVolumeReturns 191 | return fakeReturns.result1, fakeReturns.result2 192 | } 193 | 194 | func (fake *FakeFilesystem) NewVolumeCallCount() int { 195 | fake.newVolumeMutex.RLock() 196 | defer fake.newVolumeMutex.RUnlock() 197 | return len(fake.newVolumeArgsForCall) 198 | } 199 | 200 | func (fake *FakeFilesystem) NewVolumeCalls(stub func(string) (volume.FilesystemInitVolume, error)) { 201 | fake.newVolumeMutex.Lock() 202 | defer fake.newVolumeMutex.Unlock() 203 | fake.NewVolumeStub = stub 204 | } 205 | 206 | func (fake *FakeFilesystem) NewVolumeArgsForCall(i int) string { 207 | fake.newVolumeMutex.RLock() 208 | defer fake.newVolumeMutex.RUnlock() 209 | argsForCall := fake.newVolumeArgsForCall[i] 210 | return argsForCall.arg1 211 | } 212 | 213 | func (fake *FakeFilesystem) NewVolumeReturns(result1 volume.FilesystemInitVolume, result2 error) { 214 | fake.newVolumeMutex.Lock() 215 | defer fake.newVolumeMutex.Unlock() 216 | fake.NewVolumeStub = nil 217 | fake.newVolumeReturns = struct { 218 | result1 volume.FilesystemInitVolume 219 | result2 error 220 | }{result1, result2} 221 | } 222 | 223 | func (fake *FakeFilesystem) NewVolumeReturnsOnCall(i int, result1 volume.FilesystemInitVolume, result2 error) { 224 | fake.newVolumeMutex.Lock() 225 | defer fake.newVolumeMutex.Unlock() 226 | fake.NewVolumeStub = nil 227 | if fake.newVolumeReturnsOnCall == nil { 228 | fake.newVolumeReturnsOnCall = make(map[int]struct { 229 | result1 volume.FilesystemInitVolume 230 | result2 error 231 | }) 232 | } 233 | fake.newVolumeReturnsOnCall[i] = struct { 234 | result1 volume.FilesystemInitVolume 235 | result2 error 236 | }{result1, result2} 237 | } 238 | 239 | func (fake *FakeFilesystem) Invocations() map[string][][]interface{} { 240 | fake.invocationsMutex.RLock() 241 | defer fake.invocationsMutex.RUnlock() 242 | fake.listVolumesMutex.RLock() 243 | defer fake.listVolumesMutex.RUnlock() 244 | fake.lookupVolumeMutex.RLock() 245 | defer fake.lookupVolumeMutex.RUnlock() 246 | fake.newVolumeMutex.RLock() 247 | defer fake.newVolumeMutex.RUnlock() 248 | copiedInvocations := map[string][][]interface{}{} 249 | for key, value := range fake.invocations { 250 | copiedInvocations[key] = value 251 | } 252 | return copiedInvocations 253 | } 254 | 255 | func (fake *FakeFilesystem) recordInvocation(key string, args []interface{}) { 256 | fake.invocationsMutex.Lock() 257 | defer fake.invocationsMutex.Unlock() 258 | if fake.invocations == nil { 259 | fake.invocations = map[string][][]interface{}{} 260 | } 261 | if fake.invocations[key] == nil { 262 | fake.invocations[key] = [][]interface{}{} 263 | } 264 | fake.invocations[key] = append(fake.invocations[key], args) 265 | } 266 | 267 | var _ volume.Filesystem = new(FakeFilesystem) 268 | -------------------------------------------------------------------------------- /volume/volumefakes/fake_lock_manager.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package volumefakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/concourse/baggageclaim/volume" 8 | ) 9 | 10 | type FakeLockManager struct { 11 | LockStub func(string) 12 | lockMutex sync.RWMutex 13 | lockArgsForCall []struct { 14 | arg1 string 15 | } 16 | UnlockStub func(string) 17 | unlockMutex sync.RWMutex 18 | unlockArgsForCall []struct { 19 | arg1 string 20 | } 21 | invocations map[string][][]interface{} 22 | invocationsMutex sync.RWMutex 23 | } 24 | 25 | func (fake *FakeLockManager) Lock(arg1 string) { 26 | fake.lockMutex.Lock() 27 | fake.lockArgsForCall = append(fake.lockArgsForCall, struct { 28 | arg1 string 29 | }{arg1}) 30 | fake.recordInvocation("Lock", []interface{}{arg1}) 31 | fake.lockMutex.Unlock() 32 | if fake.LockStub != nil { 33 | fake.LockStub(arg1) 34 | } 35 | } 36 | 37 | func (fake *FakeLockManager) LockCallCount() int { 38 | fake.lockMutex.RLock() 39 | defer fake.lockMutex.RUnlock() 40 | return len(fake.lockArgsForCall) 41 | } 42 | 43 | func (fake *FakeLockManager) LockCalls(stub func(string)) { 44 | fake.lockMutex.Lock() 45 | defer fake.lockMutex.Unlock() 46 | fake.LockStub = stub 47 | } 48 | 49 | func (fake *FakeLockManager) LockArgsForCall(i int) string { 50 | fake.lockMutex.RLock() 51 | defer fake.lockMutex.RUnlock() 52 | argsForCall := fake.lockArgsForCall[i] 53 | return argsForCall.arg1 54 | } 55 | 56 | func (fake *FakeLockManager) Unlock(arg1 string) { 57 | fake.unlockMutex.Lock() 58 | fake.unlockArgsForCall = append(fake.unlockArgsForCall, struct { 59 | arg1 string 60 | }{arg1}) 61 | fake.recordInvocation("Unlock", []interface{}{arg1}) 62 | fake.unlockMutex.Unlock() 63 | if fake.UnlockStub != nil { 64 | fake.UnlockStub(arg1) 65 | } 66 | } 67 | 68 | func (fake *FakeLockManager) UnlockCallCount() int { 69 | fake.unlockMutex.RLock() 70 | defer fake.unlockMutex.RUnlock() 71 | return len(fake.unlockArgsForCall) 72 | } 73 | 74 | func (fake *FakeLockManager) UnlockCalls(stub func(string)) { 75 | fake.unlockMutex.Lock() 76 | defer fake.unlockMutex.Unlock() 77 | fake.UnlockStub = stub 78 | } 79 | 80 | func (fake *FakeLockManager) UnlockArgsForCall(i int) string { 81 | fake.unlockMutex.RLock() 82 | defer fake.unlockMutex.RUnlock() 83 | argsForCall := fake.unlockArgsForCall[i] 84 | return argsForCall.arg1 85 | } 86 | 87 | func (fake *FakeLockManager) Invocations() map[string][][]interface{} { 88 | fake.invocationsMutex.RLock() 89 | defer fake.invocationsMutex.RUnlock() 90 | fake.lockMutex.RLock() 91 | defer fake.lockMutex.RUnlock() 92 | fake.unlockMutex.RLock() 93 | defer fake.unlockMutex.RUnlock() 94 | copiedInvocations := map[string][][]interface{}{} 95 | for key, value := range fake.invocations { 96 | copiedInvocations[key] = value 97 | } 98 | return copiedInvocations 99 | } 100 | 101 | func (fake *FakeLockManager) recordInvocation(key string, args []interface{}) { 102 | fake.invocationsMutex.Lock() 103 | defer fake.invocationsMutex.Unlock() 104 | if fake.invocations == nil { 105 | fake.invocations = map[string][][]interface{}{} 106 | } 107 | if fake.invocations[key] == nil { 108 | fake.invocations[key] = [][]interface{}{} 109 | } 110 | fake.invocations[key] = append(fake.invocations[key], args) 111 | } 112 | 113 | var _ volume.LockManager = new(FakeLockManager) 114 | -------------------------------------------------------------------------------- /volume/volumefakes/fake_strategy.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package volumefakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "code.cloudfoundry.org/lager" 8 | "github.com/concourse/baggageclaim/volume" 9 | ) 10 | 11 | type FakeStrategy struct { 12 | MaterializeStub func(lager.Logger, string, volume.Filesystem, volume.Streamer) (volume.FilesystemInitVolume, error) 13 | materializeMutex sync.RWMutex 14 | materializeArgsForCall []struct { 15 | arg1 lager.Logger 16 | arg2 string 17 | arg3 volume.Filesystem 18 | arg4 volume.Streamer 19 | } 20 | materializeReturns struct { 21 | result1 volume.FilesystemInitVolume 22 | result2 error 23 | } 24 | materializeReturnsOnCall map[int]struct { 25 | result1 volume.FilesystemInitVolume 26 | result2 error 27 | } 28 | invocations map[string][][]interface{} 29 | invocationsMutex sync.RWMutex 30 | } 31 | 32 | func (fake *FakeStrategy) Materialize(arg1 lager.Logger, arg2 string, arg3 volume.Filesystem, arg4 volume.Streamer) (volume.FilesystemInitVolume, error) { 33 | fake.materializeMutex.Lock() 34 | ret, specificReturn := fake.materializeReturnsOnCall[len(fake.materializeArgsForCall)] 35 | fake.materializeArgsForCall = append(fake.materializeArgsForCall, struct { 36 | arg1 lager.Logger 37 | arg2 string 38 | arg3 volume.Filesystem 39 | arg4 volume.Streamer 40 | }{arg1, arg2, arg3, arg4}) 41 | fake.recordInvocation("Materialize", []interface{}{arg1, arg2, arg3, arg4}) 42 | fake.materializeMutex.Unlock() 43 | if fake.MaterializeStub != nil { 44 | return fake.MaterializeStub(arg1, arg2, arg3, arg4) 45 | } 46 | if specificReturn { 47 | return ret.result1, ret.result2 48 | } 49 | fakeReturns := fake.materializeReturns 50 | return fakeReturns.result1, fakeReturns.result2 51 | } 52 | 53 | func (fake *FakeStrategy) MaterializeCallCount() int { 54 | fake.materializeMutex.RLock() 55 | defer fake.materializeMutex.RUnlock() 56 | return len(fake.materializeArgsForCall) 57 | } 58 | 59 | func (fake *FakeStrategy) MaterializeCalls(stub func(lager.Logger, string, volume.Filesystem, volume.Streamer) (volume.FilesystemInitVolume, error)) { 60 | fake.materializeMutex.Lock() 61 | defer fake.materializeMutex.Unlock() 62 | fake.MaterializeStub = stub 63 | } 64 | 65 | func (fake *FakeStrategy) MaterializeArgsForCall(i int) (lager.Logger, string, volume.Filesystem, volume.Streamer) { 66 | fake.materializeMutex.RLock() 67 | defer fake.materializeMutex.RUnlock() 68 | argsForCall := fake.materializeArgsForCall[i] 69 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 70 | } 71 | 72 | func (fake *FakeStrategy) MaterializeReturns(result1 volume.FilesystemInitVolume, result2 error) { 73 | fake.materializeMutex.Lock() 74 | defer fake.materializeMutex.Unlock() 75 | fake.MaterializeStub = nil 76 | fake.materializeReturns = struct { 77 | result1 volume.FilesystemInitVolume 78 | result2 error 79 | }{result1, result2} 80 | } 81 | 82 | func (fake *FakeStrategy) MaterializeReturnsOnCall(i int, result1 volume.FilesystemInitVolume, result2 error) { 83 | fake.materializeMutex.Lock() 84 | defer fake.materializeMutex.Unlock() 85 | fake.MaterializeStub = nil 86 | if fake.materializeReturnsOnCall == nil { 87 | fake.materializeReturnsOnCall = make(map[int]struct { 88 | result1 volume.FilesystemInitVolume 89 | result2 error 90 | }) 91 | } 92 | fake.materializeReturnsOnCall[i] = struct { 93 | result1 volume.FilesystemInitVolume 94 | result2 error 95 | }{result1, result2} 96 | } 97 | 98 | func (fake *FakeStrategy) Invocations() map[string][][]interface{} { 99 | fake.invocationsMutex.RLock() 100 | defer fake.invocationsMutex.RUnlock() 101 | fake.materializeMutex.RLock() 102 | defer fake.materializeMutex.RUnlock() 103 | copiedInvocations := map[string][][]interface{}{} 104 | for key, value := range fake.invocations { 105 | copiedInvocations[key] = value 106 | } 107 | return copiedInvocations 108 | } 109 | 110 | func (fake *FakeStrategy) recordInvocation(key string, args []interface{}) { 111 | fake.invocationsMutex.Lock() 112 | defer fake.invocationsMutex.Unlock() 113 | if fake.invocations == nil { 114 | fake.invocations = map[string][][]interface{}{} 115 | } 116 | if fake.invocations[key] == nil { 117 | fake.invocations[key] = [][]interface{}{} 118 | } 119 | fake.invocations[key] = append(fake.invocations[key], args) 120 | } 121 | 122 | var _ volume.Strategy = new(FakeStrategy) 123 | -------------------------------------------------------------------------------- /volume/volumefakes/fake_streamer.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package volumefakes 3 | 4 | import ( 5 | "io" 6 | "sync" 7 | 8 | "github.com/concourse/baggageclaim/volume" 9 | ) 10 | 11 | type FakeStreamer struct { 12 | InStub func(io.Reader, string, bool) (bool, error) 13 | inMutex sync.RWMutex 14 | inArgsForCall []struct { 15 | arg1 io.Reader 16 | arg2 string 17 | arg3 bool 18 | } 19 | inReturns struct { 20 | result1 bool 21 | result2 error 22 | } 23 | inReturnsOnCall map[int]struct { 24 | result1 bool 25 | result2 error 26 | } 27 | OutStub func(io.Writer, string, bool) error 28 | outMutex sync.RWMutex 29 | outArgsForCall []struct { 30 | arg1 io.Writer 31 | arg2 string 32 | arg3 bool 33 | } 34 | outReturns struct { 35 | result1 error 36 | } 37 | outReturnsOnCall map[int]struct { 38 | result1 error 39 | } 40 | invocations map[string][][]interface{} 41 | invocationsMutex sync.RWMutex 42 | } 43 | 44 | func (fake *FakeStreamer) In(arg1 io.Reader, arg2 string, arg3 bool) (bool, error) { 45 | fake.inMutex.Lock() 46 | ret, specificReturn := fake.inReturnsOnCall[len(fake.inArgsForCall)] 47 | fake.inArgsForCall = append(fake.inArgsForCall, struct { 48 | arg1 io.Reader 49 | arg2 string 50 | arg3 bool 51 | }{arg1, arg2, arg3}) 52 | fake.recordInvocation("In", []interface{}{arg1, arg2, arg3}) 53 | fake.inMutex.Unlock() 54 | if fake.InStub != nil { 55 | return fake.InStub(arg1, arg2, arg3) 56 | } 57 | if specificReturn { 58 | return ret.result1, ret.result2 59 | } 60 | fakeReturns := fake.inReturns 61 | return fakeReturns.result1, fakeReturns.result2 62 | } 63 | 64 | func (fake *FakeStreamer) InCallCount() int { 65 | fake.inMutex.RLock() 66 | defer fake.inMutex.RUnlock() 67 | return len(fake.inArgsForCall) 68 | } 69 | 70 | func (fake *FakeStreamer) InCalls(stub func(io.Reader, string, bool) (bool, error)) { 71 | fake.inMutex.Lock() 72 | defer fake.inMutex.Unlock() 73 | fake.InStub = stub 74 | } 75 | 76 | func (fake *FakeStreamer) InArgsForCall(i int) (io.Reader, string, bool) { 77 | fake.inMutex.RLock() 78 | defer fake.inMutex.RUnlock() 79 | argsForCall := fake.inArgsForCall[i] 80 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 81 | } 82 | 83 | func (fake *FakeStreamer) InReturns(result1 bool, result2 error) { 84 | fake.inMutex.Lock() 85 | defer fake.inMutex.Unlock() 86 | fake.InStub = nil 87 | fake.inReturns = struct { 88 | result1 bool 89 | result2 error 90 | }{result1, result2} 91 | } 92 | 93 | func (fake *FakeStreamer) InReturnsOnCall(i int, result1 bool, result2 error) { 94 | fake.inMutex.Lock() 95 | defer fake.inMutex.Unlock() 96 | fake.InStub = nil 97 | if fake.inReturnsOnCall == nil { 98 | fake.inReturnsOnCall = make(map[int]struct { 99 | result1 bool 100 | result2 error 101 | }) 102 | } 103 | fake.inReturnsOnCall[i] = struct { 104 | result1 bool 105 | result2 error 106 | }{result1, result2} 107 | } 108 | 109 | func (fake *FakeStreamer) Out(arg1 io.Writer, arg2 string, arg3 bool) error { 110 | fake.outMutex.Lock() 111 | ret, specificReturn := fake.outReturnsOnCall[len(fake.outArgsForCall)] 112 | fake.outArgsForCall = append(fake.outArgsForCall, struct { 113 | arg1 io.Writer 114 | arg2 string 115 | arg3 bool 116 | }{arg1, arg2, arg3}) 117 | fake.recordInvocation("Out", []interface{}{arg1, arg2, arg3}) 118 | fake.outMutex.Unlock() 119 | if fake.OutStub != nil { 120 | return fake.OutStub(arg1, arg2, arg3) 121 | } 122 | if specificReturn { 123 | return ret.result1 124 | } 125 | fakeReturns := fake.outReturns 126 | return fakeReturns.result1 127 | } 128 | 129 | func (fake *FakeStreamer) OutCallCount() int { 130 | fake.outMutex.RLock() 131 | defer fake.outMutex.RUnlock() 132 | return len(fake.outArgsForCall) 133 | } 134 | 135 | func (fake *FakeStreamer) OutCalls(stub func(io.Writer, string, bool) error) { 136 | fake.outMutex.Lock() 137 | defer fake.outMutex.Unlock() 138 | fake.OutStub = stub 139 | } 140 | 141 | func (fake *FakeStreamer) OutArgsForCall(i int) (io.Writer, string, bool) { 142 | fake.outMutex.RLock() 143 | defer fake.outMutex.RUnlock() 144 | argsForCall := fake.outArgsForCall[i] 145 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 146 | } 147 | 148 | func (fake *FakeStreamer) OutReturns(result1 error) { 149 | fake.outMutex.Lock() 150 | defer fake.outMutex.Unlock() 151 | fake.OutStub = nil 152 | fake.outReturns = struct { 153 | result1 error 154 | }{result1} 155 | } 156 | 157 | func (fake *FakeStreamer) OutReturnsOnCall(i int, result1 error) { 158 | fake.outMutex.Lock() 159 | defer fake.outMutex.Unlock() 160 | fake.OutStub = nil 161 | if fake.outReturnsOnCall == nil { 162 | fake.outReturnsOnCall = make(map[int]struct { 163 | result1 error 164 | }) 165 | } 166 | fake.outReturnsOnCall[i] = struct { 167 | result1 error 168 | }{result1} 169 | } 170 | 171 | func (fake *FakeStreamer) Invocations() map[string][][]interface{} { 172 | fake.invocationsMutex.RLock() 173 | defer fake.invocationsMutex.RUnlock() 174 | fake.inMutex.RLock() 175 | defer fake.inMutex.RUnlock() 176 | fake.outMutex.RLock() 177 | defer fake.outMutex.RUnlock() 178 | copiedInvocations := map[string][][]interface{}{} 179 | for key, value := range fake.invocations { 180 | copiedInvocations[key] = value 181 | } 182 | return copiedInvocations 183 | } 184 | 185 | func (fake *FakeStreamer) recordInvocation(key string, args []interface{}) { 186 | fake.invocationsMutex.Lock() 187 | defer fake.invocationsMutex.Unlock() 188 | if fake.invocations == nil { 189 | fake.invocations = map[string][][]interface{}{} 190 | } 191 | if fake.invocations[key] == nil { 192 | fake.invocations[key] = [][]interface{}{} 193 | } 194 | fake.invocations[key] = append(fake.invocations[key], args) 195 | } 196 | 197 | var _ volume.Streamer = new(FakeStreamer) 198 | --------------------------------------------------------------------------------