chmod.go 3.42 KB
Newer Older
songlinfeng's avatar
songlinfeng committed
1
2
3
4
5
6
7
8
9
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
/**
# Copyright (c) 2024, HCUOpt CORPORATION.  All rights reserved.
**/

package chmod

import (
	"dtk-container-toolkit/internal/logger"
	"dtk-container-toolkit/internal/oci"
	"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
}