Skip to content

Commit 9ce95ee

Browse files
authored
chore: bring back advanced mock shell command (#387)
1 parent 79a66b1 commit 9ce95ee

4 files changed

Lines changed: 349 additions & 0 deletions

File tree

internal/app/azldev/cmds/advanced/advanced.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ func OnAppInit(app *azldev.App) {
1919

2020
app.AddTopLevelCommand(cmd)
2121
mcpOnAppInit(app, cmd)
22+
mockOnAppInit(app, cmd)
2223
wgetOnAppInit(app, cmd)
2324
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package advanced
5+
6+
import (
7+
"fmt"
8+
"os"
9+
10+
"github.com/microsoft/azldev/internal/app/azldev"
11+
"github.com/microsoft/azldev/internal/app/azldev/core/buildenvfactory"
12+
"github.com/microsoft/azldev/internal/buildenv"
13+
"github.com/microsoft/azldev/internal/rpm/mock"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
func mockOnAppInit(_ *azldev.App, parentCmd *cobra.Command) {
18+
parentCmd.AddCommand(NewMockCmd())
19+
}
20+
21+
// Constructs a [cobra.Command] for the "mock" subcommand hierarchy.
22+
func NewMockCmd() *cobra.Command {
23+
cmd := &cobra.Command{
24+
Use: "mock",
25+
Short: "Run RPM mock tool",
26+
}
27+
28+
cmd.AddCommand(NewBuildRPMCmd())
29+
cmd.AddCommand(NewShellCmd())
30+
31+
return cmd
32+
}
33+
34+
// Options controlling how to run mock commands.
35+
type MockCmdOptions struct {
36+
// Path to the .cfg file to use with mock.
37+
MockConfigPath string
38+
}
39+
40+
// Options controlling how to build an RPM from a source RPM.
41+
type BuildRPMOptions struct {
42+
// Common mock options.
43+
MockCmdOptions
44+
45+
// Path to the SRPM to build.
46+
SRPMPath string
47+
// Path to the output directory for final RPM files.
48+
OutputDirPath string
49+
}
50+
51+
// Constructs a [cobra.Command] for the "mock build-rpms" subcommand.
52+
func NewBuildRPMCmd() *cobra.Command {
53+
options := &BuildRPMOptions{}
54+
55+
// We don't *require* a valid project configuration, but may use one if it's available.
56+
cmd := &cobra.Command{
57+
Use: "build-rpms",
58+
Short: "Use mock to build an RPM",
59+
RunE: azldev.RunFuncWithoutRequiredConfig(func(env *azldev.Env) (results interface{}, err error) {
60+
return BuildRPMS(env, options)
61+
}),
62+
}
63+
64+
cmd.Flags().StringVarP(&options.MockConfigPath, "config", "c", "", "Path to the mock .cfg file")
65+
cmd.Flags().StringVar(&options.SRPMPath, "srpm", "", "Path to the SRPM to build")
66+
cmd.Flags().StringVarP(&options.OutputDirPath, "output-dir", "o", "", "Path to output directory")
67+
68+
_ = cmd.MarkFlagRequired("srpm")
69+
_ = cmd.MarkFlagRequired("output-dir")
70+
71+
return cmd
72+
}
73+
74+
// Options controlling how to run a shell command in a mock environment.
75+
type ShellOptions struct {
76+
// Common mock options.
77+
MockCmdOptions
78+
79+
// Whether or not to enable external network access from within the mock root the shell is executed in.
80+
EnableNetwork bool
81+
}
82+
83+
// Constructs a [cobra.Command] for the 'mock shell' command.
84+
func NewShellCmd() *cobra.Command {
85+
options := &ShellOptions{}
86+
87+
// We don't *require* a valid project configuration, but may use one if it's available.
88+
cmd := &cobra.Command{
89+
Use: "shell",
90+
Short: "Enter mock shell",
91+
RunE: azldev.RunFuncWithoutRequiredConfigWithExtraArgs(
92+
func(env *azldev.Env, extraArgs []string) (results interface{}, err error) {
93+
return true, RunShell(env, options, extraArgs)
94+
},
95+
),
96+
}
97+
98+
cmd.Flags().StringVarP(&options.MockConfigPath, "config", "c", "", "Path to the mock .cfg file")
99+
cmd.Flags().BoolVar(&options.EnableNetwork, "enable-network", false, "Enable network access in the mock root")
100+
101+
return cmd
102+
}
103+
104+
// Builds RPMs from sources, using options.
105+
func BuildRPMS(env *azldev.Env, options *BuildRPMOptions) (results interface{}, err error) {
106+
runner, err := makeMockRunner(env, &options.MockCmdOptions)
107+
if err != nil {
108+
return results, err
109+
}
110+
111+
buildOptions := mock.RPMBuildOptions{}
112+
113+
evt := env.StartEvent("Building RPM with mock",
114+
"srpmPath", options.SRPMPath, "outputDirPath", options.OutputDirPath)
115+
116+
defer evt.End()
117+
118+
// Build!
119+
err = runner.BuildRPM(env, options.SRPMPath, options.OutputDirPath, buildOptions)
120+
if err != nil {
121+
return results, fmt.Errorf("failed to build RPM: %w", err)
122+
}
123+
124+
return true, nil
125+
}
126+
127+
// Executes an interactive shell within a mock root. Uses the provided [ShellOptions] to configure the shell.
128+
func RunShell(env *azldev.Env, options *ShellOptions, extraArgs []string) error {
129+
runner, err := makeMockRunner(env, &options.MockCmdOptions)
130+
if err != nil {
131+
return err
132+
}
133+
134+
if options.EnableNetwork {
135+
runner.EnableNetwork()
136+
}
137+
138+
cmd, err := runner.CmdInChroot(env, extraArgs, true /*interactive*/)
139+
if err != nil {
140+
return fmt.Errorf("failed to create shell command: %w", err)
141+
}
142+
143+
cmd.SetStdin(os.Stdin)
144+
cmd.SetStdout(os.Stdout)
145+
cmd.SetStderr(os.Stderr)
146+
147+
err = cmd.Run(env)
148+
if err != nil {
149+
return fmt.Errorf("failed to run shell: %w", err)
150+
}
151+
152+
return nil
153+
}
154+
155+
func makeMockRunner(env *azldev.Env, options *MockCmdOptions) (runner *mock.Runner, err error) {
156+
// If we have an explicit config path, then use it.
157+
if options.MockConfigPath != "" {
158+
return mock.NewRunner(env, options.MockConfigPath), nil
159+
}
160+
161+
// Otherwise, try to find the right one for this environment.
162+
factory, err := buildenvfactory.NewMockRootFactoryForEnv(env)
163+
if err != nil {
164+
return nil, fmt.Errorf("failed to create mock root factory: %w", err)
165+
}
166+
167+
root, err := factory.CreateMockRoot(buildenv.CreateOptions{})
168+
if err != nil {
169+
return nil, fmt.Errorf("failed to create mock root: %w", err)
170+
}
171+
172+
return root.GetRunner(), nil
173+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package advanced_test
5+
6+
import (
7+
"os/exec"
8+
"slices"
9+
"testing"
10+
11+
"github.com/microsoft/azldev/internal/app/azldev/cmds/advanced"
12+
"github.com/microsoft/azldev/internal/app/azldev/core/testutils"
13+
"github.com/microsoft/azldev/internal/projectconfig"
14+
"github.com/microsoft/azldev/internal/utils/fileutils"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func TestNewMockCommand(t *testing.T) {
20+
cmd := advanced.NewMockCmd()
21+
require.NotNil(t, cmd)
22+
assert.Equal(t, "mock", cmd.Use)
23+
}
24+
25+
func TestNewBuildRPMCmd(t *testing.T) {
26+
cmd := advanced.NewBuildRPMCmd()
27+
require.NotNil(t, cmd)
28+
assert.Equal(t, "build-rpms", cmd.Use)
29+
}
30+
31+
func TestNewShellCmd(t *testing.T) {
32+
cmd := advanced.NewShellCmd()
33+
require.NotNil(t, cmd)
34+
assert.Equal(t, "shell", cmd.Use)
35+
}
36+
37+
func TestBuildRPMS(t *testing.T) {
38+
const (
39+
testMockConfigPath = "/mock/mock.cfg"
40+
testSRPMPath = "/input/test.srpm"
41+
testOutputDirPath = "/output"
42+
)
43+
44+
t.Run("NoProjectConfig", func(t *testing.T) {
45+
// Construct a new env with no configuration.
46+
testEnv := testutils.NewTestEnv(t)
47+
48+
// Pretend that "mock" exists.
49+
testEnv.CmdFactory.RegisterCommandInSearchPath("mock")
50+
51+
rpmOptions := &advanced.BuildRPMOptions{
52+
MockCmdOptions: advanced.MockCmdOptions{
53+
MockConfigPath: testMockConfigPath,
54+
},
55+
SRPMPath: testSRPMPath,
56+
OutputDirPath: testOutputDirPath,
57+
}
58+
59+
// Confirm that we can "build" RPMs, even without a valid loaded configuration.
60+
result, err := advanced.BuildRPMS(testEnv.Env, rpmOptions)
61+
require.NoError(t, err)
62+
require.Equal(t, true, result)
63+
})
64+
65+
t.Run("ValidProjectConfig", func(t *testing.T) {
66+
testEnv := testutils.NewTestEnv(t)
67+
68+
testEnv.Config.Project.DefaultDistro = projectconfig.DistroReference{
69+
Name: "test-distro",
70+
Version: "1.0",
71+
}
72+
73+
testEnv.Config.Distros["test-distro"] = projectconfig.DistroDefinition{
74+
Versions: map[string]projectconfig.DistroVersionDefinition{
75+
"1.0": {
76+
MockConfigPath: testMockConfigPath,
77+
},
78+
},
79+
}
80+
81+
// Pretend that "mock" exists.
82+
testEnv.CmdFactory.RegisterCommandInSearchPath("mock")
83+
84+
rpmOptions := &advanced.BuildRPMOptions{
85+
SRPMPath: testSRPMPath,
86+
OutputDirPath: testOutputDirPath,
87+
}
88+
89+
require.NoError(t, fileutils.WriteFile(testEnv.FS(), testMockConfigPath, []byte{}, 0o600))
90+
91+
// Confirm that we can "build" RPMs without an explicit mock config file, because
92+
// the loaded project config provides that.
93+
result, err := advanced.BuildRPMS(testEnv.Env, rpmOptions)
94+
require.NoError(t, err)
95+
require.Equal(t, true, result)
96+
})
97+
}
98+
99+
func TestRunShell(t *testing.T) {
100+
// Construct a new env.
101+
testEnv := testutils.NewTestEnv(t)
102+
103+
// Pretend that "mock" exists.
104+
testEnv.CmdFactory.RegisterCommandInSearchPath("mock")
105+
106+
// Keep track of what gets launched.
107+
cmds := [][]string{}
108+
testEnv.CmdFactory.RunHandler = func(cmd *exec.Cmd) error {
109+
cmds = append(cmds, cmd.Args)
110+
111+
return nil
112+
}
113+
114+
err := advanced.RunShell(testEnv.Env, &advanced.ShellOptions{}, []string{})
115+
require.NoError(t, err)
116+
117+
found := false
118+
119+
for _, cmd := range cmds {
120+
if slices.Contains(cmd, "mock") && slices.Contains(cmd, "--shell") {
121+
found = true
122+
123+
break
124+
}
125+
}
126+
127+
require.True(t, found, "Expected 'mock --shell' command to be executed")
128+
}

scenario/mock_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//go:build scenario
5+
6+
package scenario_tests
7+
8+
import (
9+
"strings"
10+
"testing"
11+
12+
"github.com/microsoft/azldev/scenario/internal/cmdtest"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
// We test running `azldev advanced mock shell` to make sure the default configuration is
17+
// enough to run with it.
18+
func TestMockShellWithDefaultConfig(t *testing.T) {
19+
t.Parallel()
20+
21+
// Skip unless doing long tests
22+
if testing.Short() {
23+
t.Skip("skipping long test")
24+
}
25+
26+
// Write out a simple script that initializes a project and runs a mock shell.
27+
// The 'whoami' command should report back 'root', which will tell us it made
28+
// it into the root environment.
29+
testScript := `
30+
set -euxo pipefail
31+
azldev project init
32+
azldev advanced mock shell whoami --verbose
33+
`
34+
35+
// NOTE: We need to run in a privileged container so 'mock' can create its nested root environment.
36+
// NOTE: We need to enable networking so 'mock' can download Azure Linux packages to build a root.
37+
results, err := cmdtest.NewScenarioTest().
38+
WithScript(strings.NewReader(testScript)).
39+
InContainer().
40+
WithPrivilege().
41+
WithNetwork().
42+
Run(t)
43+
44+
require.NoError(t, err)
45+
results.AssertZeroExitCode(t)
46+
results.AssertStdoutContains(t, "root")
47+
}

0 commit comments

Comments
 (0)