chmod.go 3.42 KB
Newer Older
songlinfeng's avatar
songlinfeng committed
1
2
3
4
5
6
7
/**
# Copyright (c) 2024, HCUOpt CORPORATION.  All rights reserved.
**/

package chmod

import (
8
9
	"dcu-container-toolkit/internal/logger"
	"dcu-container-toolkit/internal/oci"
songlinfeng's avatar
songlinfeng committed
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/urfave/cli/v2"
)

type command struct {
	logger logger.Interface
}

type config struct {
	paths         cli.StringSlice
	modeStr       string
	mode          fs.FileMode
	containerSpec string
}

// NewCommand constructs a chmod command with the specified logger
func NewCommand(logger logger.Interface) *cli.Command {
	c := command{
		logger: logger,
	}
	return c.build()
}

// build the chmod command
func (m command) build() *cli.Command {
	cfg := config{}

	// Create the 'chmod' command
	c := cli.Command{
		Name:  "chmod",
		Usage: "Set the permissions of folders in the container by running chmod. The container root is prefixed to the specified paths.",
		Before: func(c *cli.Context) error {
			return validateFlags(c, &cfg)
		},
		Action: func(c *cli.Context) error {
			return m.run(c, &cfg)
		},
	}

	c.Flags = []cli.Flag{
		&cli.StringSliceFlag{
			Name:        "path",
			Usage:       "Specify a path to apply the specified mode to",
			Destination: &cfg.paths,
		},
		&cli.StringFlag{
			Name:        "mode",
			Usage:       "Specify the file mode",
			Destination: &cfg.modeStr,
		},
		&cli.StringFlag{
			Name:        "container-spec",
			Usage:       "Specify the path to the OCI container spec. If empty or '-' the spec will be read from STDIN",
			Destination: &cfg.containerSpec,
		},
	}

	return &c
}

func validateFlags(c *cli.Context, cfg *config) error {
	if strings.TrimSpace(cfg.modeStr) == "" {
		return fmt.Errorf("a non-empty mode must be specified")
	}

	modeInt, err := strconv.ParseUint(cfg.modeStr, 8, 32)
	if err != nil {
		return fmt.Errorf("failed to parse mode as octal: %v", err)
	}
	cfg.mode = fs.FileMode(modeInt)

	for _, p := range cfg.paths.Value() {
		if strings.TrimSpace(p) == "" {
			return fmt.Errorf("paths must not be empty")
		}
	}

	return nil
}

func (m command) run(c *cli.Context, cfg *config) error {
	s, err := oci.LoadContainerState(cfg.containerSpec)
	if err != nil {
		return fmt.Errorf("failed to load container state: %v", err)
	}

	containerRoot, err := s.GetContainerRoot()
	if err != nil {
		return fmt.Errorf("failed to determined container root: %v", err)
	}
	if containerRoot == "" {
		return fmt.Errorf("empty container root detected")
	}

	paths := m.getPaths(containerRoot, cfg.paths.Value(), cfg.mode)
	if len(paths) == 0 {
		m.logger.Debugf("No paths specified; exiting")
		return nil
	}

	for _, path := range paths {
		err = os.Chmod(path, cfg.mode)
		// in some cases this is not an issue (e.g. whole /dev mounted), see #143
		if errors.Is(err, fs.ErrPermission) {
			m.logger.Debugf("Ignoring permission error with chmod: %v", err)
			err = nil
		}
	}

	return err
}

// getPaths updates the specified paths relative to the root.
func (m command) getPaths(root string, paths []string, desiredMode fs.FileMode) []string {
	var pathsInRoot []string
	for _, f := range paths {
		path := filepath.Join(root, f)
		stat, err := os.Stat(path)
		if err != nil {
			m.logger.Debugf("Skipping path %q: %v", path, err)
			continue
		}
		if (stat.Mode()&(fs.ModePerm|fs.ModeSetuid|fs.ModeSetgid|fs.ModeSticky))^desiredMode == 0 {
			m.logger.Debugf("Skipping path %q: already desired mode", path)
			continue
		}
		pathsInRoot = append(pathsInRoot, path)
	}

	return pathsInRoot
}