Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(daemon): Add support for PEBBLE_COPY_ONCE. #352

Merged
merged 5 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ pebble run
...
```

To initialise the `$PEBBLE` directory with the contents of another, in a one time copy, set the `PEBBLE_COPY_ONCE` environment
variable to the source directory. This will only copy the contents if the target directory, `$PEBBLE`, is empty.

### Viewing, starting, and stopping services

You can view the status of one or more services by using `pebble services`:
Expand Down
4 changes: 4 additions & 0 deletions internals/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,10 @@ func getEnvPaths() (pebbleDir string, socketPath string) {
return pebbleDir, socketPath
}

func getCopySource() string {
return os.Getenv("PEBBLE_COPY_ONCE")
}

type cliState struct {
NoticesLastListed time.Time `json:"notices-last-listed"`
NoticesLastOkayed time.Time `json:"notices-last-okayed"`
Expand Down
25 changes: 25 additions & 0 deletions internals/cli/cmd_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ func runDaemon(rcmd *cmdRun, ch chan os.Signal, ready chan<- func()) error {
return err
}
}
err := maybeCopyPebbleDir(pebbleDir, getCopySource())
if err != nil {
return err
}

dopts := daemon.Options{
Dir: pebbleDir,
SocketPath: socketPath,
Expand Down Expand Up @@ -275,3 +280,23 @@ func convertArgs(args [][]string) (map[string][]string, error) {

return mappedArgs, nil
}

func maybeCopyPebbleDir(destDir, srcDir string) error {
if srcDir == "" {
return nil
}
entries, err := os.ReadDir(destDir)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
} else if len(entries) != 0 {
// Skip non-empty dir.
return nil
}
fsys := os.DirFS(srcDir)
// TODO: replace with os.CopyFS when we're using Go 1.23
err = copyFS(destDir, fsys)
if err != nil {
return fmt.Errorf("cannot copy %q to %q: %w", srcDir, destDir, err)
}
return nil
}
105 changes: 105 additions & 0 deletions internals/cli/cmd_run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) 2024 Canonical Ltd
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package cli_test

import (
"io/fs"
"os"
"path"

. "gopkg.in/check.v1"

"github.com/canonical/pebble/internals/cli"
)

func (s *PebbleSuite) TestMaybeCopyPebbleDir(c *C) {
src := c.MkDir()
err := os.MkdirAll(path.Join(src, "a", "b", "c"), 0700)
c.Assert(err, IsNil)
err = os.WriteFile(path.Join(src, "a", "b", "c", "a.yaml"), []byte("# hi\n"), 0666)
c.Assert(err, IsNil)
err = os.WriteFile(path.Join(src, "a.yaml"), []byte("# bye\n"), 0666)
c.Assert(err, IsNil)

dst := c.MkDir()
err = cli.MaybeCopyPebbleDir(dst, src)
c.Assert(err, IsNil)

got := map[string]bool{}
dstFS := os.DirFS(dst)
err = fs.WalkDir(dstFS, ".", func(path string, d fs.DirEntry, err error) error {
switch path {
case ".", "a", "a/b", "a/b/c":
case "a.yaml":
c.Check(got[path], Equals, false)
data, err := fs.ReadFile(dstFS, path)
c.Check(err, IsNil)
c.Check(data, DeepEquals, []byte("# bye\n"))
got[path] = true
case "a/b/c/a.yaml":
c.Check(got[path], Equals, false)
data, err := fs.ReadFile(dstFS, path)
c.Check(err, IsNil)
c.Check(data, DeepEquals, []byte("# hi\n"))
got[path] = true
default:
c.Errorf("bad path %s", path)
}
return err
})
c.Assert(err, IsNil)
c.Assert(got, DeepEquals, map[string]bool{
"a.yaml": true,
"a/b/c/a.yaml": true,
})
}

func (s *PebbleSuite) TestMaybeCopyPebbleDirNoCopy(c *C) {
src := c.MkDir()
err := os.MkdirAll(path.Join(src, "a", "b", "c"), 0700)
c.Assert(err, IsNil)
err = os.WriteFile(path.Join(src, "a", "b", "c", "a.yaml"), []byte("# hi\n"), 0666)
c.Assert(err, IsNil)
err = os.WriteFile(path.Join(src, "a.yaml"), []byte("# bye\n"), 0666)
c.Assert(err, IsNil)

dst := c.MkDir()
err = os.WriteFile(path.Join(dst, "a.yaml"), []byte("# no\n"), 0666)
c.Assert(err, IsNil)

err = cli.MaybeCopyPebbleDir(dst, src)
c.Assert(err, IsNil)

got := map[string]bool{}
dstFS := os.DirFS(dst)
err = fs.WalkDir(dstFS, ".", func(path string, d fs.DirEntry, err error) error {
switch path {
case ".":
case "a.yaml":
c.Check(got[path], Equals, false)
data, err := fs.ReadFile(dstFS, path)
c.Check(err, IsNil)
c.Check(data, DeepEquals, []byte("# no\n"))
got[path] = true
default:
c.Errorf("bad path %s", path)
}
return err
})
c.Assert(err, IsNil)
c.Assert(got, DeepEquals, map[string]bool{
"a.yaml": true,
})
}
95 changes: 95 additions & 0 deletions internals/cli/copyfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) 2009 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// - Redistributions of source code must retain the above copyright
//
// notice, this list of conditions and the following disclaimer.
// - Redistributions in binary form must reproduce the above
//
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// - Neither the name of Google Inc. nor the names of its
//
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package cli

import (
"io"
"io/fs"
"os"
"path/filepath"
)

// Implementation from https://go-review.googlesource.com/c/go/+/558995 until accepted into go.

// copyFS copies the file system fsys into the directory dir,
// creating dir if necessary.
//
// Newly created directories and files have their default modes
// where any bits from the file in fsys that are not part of the
// standard read, write, and execute permissions will be zeroed
// out, and standard read and write permissions are set for owner,
// group, and others while retaining any existing execute bits from
// the file in fsys.
//
// Symbolic links in fsys are not supported, a *PathError with Err set
// to ErrInvalid is returned on symlink.
//
// Copying stops at and returns the first error encountered.
func copyFS(dir string, fsys fs.FS) error {
return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}

newPath := filepath.Join(dir, path)
if d.IsDir() {
return os.MkdirAll(newPath, 0777)
}

// TODO(panjf2000): handle symlinks with the help of fs.ReadLinkFS
// once https://go.dev/issue/49580 is done.
// we also need safefilepath.IsLocal from https://go.dev/cl/564295.
if !d.Type().IsRegular() {
return &fs.PathError{Op: "CopyFS", Path: path, Err: fs.ErrInvalid}
}

r, err := fsys.Open(path)
if err != nil {
return err
}
defer r.Close()
info, err := r.Stat()
if err != nil {
return err
}
w, err := os.OpenFile(newPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666|info.Mode()&0777)
if err != nil {
return err
}

if _, err := io.Copy(w, r); err != nil {
w.Close()
return &fs.PathError{Op: "Copy", Path: newPath, Err: err}
}
return w.Close()
})
}
2 changes: 2 additions & 0 deletions internals/cli/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ var (
MaybePresentWarnings = maybePresentWarnings

GetEnvPaths = getEnvPaths

MaybeCopyPebbleDir = maybeCopyPebbleDir
)

func FakeIsStdoutTTY(t bool) (restore func()) {
Expand Down
Loading