From 8b6def9e979eb175d409878df395d2954c61375b Mon Sep 17 00:00:00 2001 From: Michael Chernov <4ernovm@gmail.com> Date: Thu, 23 Nov 2023 13:23:42 +0200 Subject: [PATCH] Add composer.json resolver (#151) * Add composer files resolver --- build/docker/alpine.Dockerfile | 15 +++++ build/docker/debian.Dockerfile | 17 +++++ .../resolution/pm/composer/cmd_factory.go | 47 ++++++++++++++ .../pm/composer/cmd_factory_test.go | 19 ++++++ internal/resolution/pm/composer/job.go | 62 ++++++++++++++++++ internal/resolution/pm/composer/job_test.go | 64 +++++++++++++++++++ internal/resolution/pm/composer/pm.go | 23 +++++++ internal/resolution/pm/composer/pm_test.go | 40 ++++++++++++ internal/resolution/pm/composer/strategy.go | 29 +++++++++ .../resolution/pm/composer/strategy_test.go | 43 +++++++++++++ .../pm/composer/testdata/cmd_factory_mock.go | 20 ++++++ internal/resolution/pm/pm.go | 2 + internal/resolution/pm/pm_test.go | 1 + .../resolution/strategy/strategy_factory.go | 3 + .../strategy/strategy_factory_test.go | 14 ++-- test/resolve/resolver_test.go | 6 ++ test/resolve/testdata/composer/composer.json | 14 ++++ 17 files changed, 413 insertions(+), 6 deletions(-) create mode 100644 internal/resolution/pm/composer/cmd_factory.go create mode 100644 internal/resolution/pm/composer/cmd_factory_test.go create mode 100644 internal/resolution/pm/composer/job.go create mode 100644 internal/resolution/pm/composer/job_test.go create mode 100644 internal/resolution/pm/composer/pm.go create mode 100644 internal/resolution/pm/composer/pm_test.go create mode 100644 internal/resolution/pm/composer/strategy.go create mode 100644 internal/resolution/pm/composer/strategy_test.go create mode 100644 internal/resolution/pm/composer/testdata/cmd_factory_mock.go create mode 100644 test/resolve/testdata/composer/composer.json diff --git a/build/docker/alpine.Dockerfile b/build/docker/alpine.Dockerfile index 76c4b782..7980ca11 100644 --- a/build/docker/alpine.Dockerfile +++ b/build/docker/alpine.Dockerfile @@ -51,5 +51,20 @@ RUN apk --no-cache --update add \ RUN dotnet --version +RUN apk add --no-cache \ + git \ + php82 \ + php82-curl \ + php82-mbstring \ + php82-openssl \ + php82-phar \ + && ln -s /usr/bin/php82 /usr/bin/php + +RUN apk add --no-cache --virtual build-dependencies curl && \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer \ + && apk del build-dependencies + +RUN php -v && composer --version + # Put copy at the end to speedup Docker build by caching previous RUNs and run those concurrently COPY --from=dev /cli/debricked /usr/bin/debricked diff --git a/build/docker/debian.Dockerfile b/build/docker/debian.Dockerfile index a9391a9b..e2b2d100 100644 --- a/build/docker/debian.Dockerfile +++ b/build/docker/debian.Dockerfile @@ -81,5 +81,22 @@ RUN apt -y update && apt -y upgrade && apt -y install openjdk-11-jre \ RUN dotnet --version +RUN apt update -y && \ + apt install lsb-release apt-transport-https ca-certificates software-properties-common -y && \ + curl -o /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg && \ + sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' && \ + apt -y clean && rm -rf /var/lib/apt/lists/* + +RUN apt -y update && apt -y install \ + php8.2 \ + php8.2-curl \ + php8.2-mbstring \ + php8.2-phar && \ + apt -y clean && rm -rf /var/lib/apt/lists/* + +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer + +RUN php -v && composer --version + # Put copy at the end to speedup Docker build by caching previous RUNs and run those concurrently COPY --from=dev /cli/debricked /usr/bin/debricked diff --git a/internal/resolution/pm/composer/cmd_factory.go b/internal/resolution/pm/composer/cmd_factory.go new file mode 100644 index 00000000..3e93b301 --- /dev/null +++ b/internal/resolution/pm/composer/cmd_factory.go @@ -0,0 +1,47 @@ +package composer + +import ( + "os" + "os/exec" + "path/filepath" +) + +type ICmdFactory interface { + MakeInstallCmd(command string, file string) (*exec.Cmd, error) +} + +type IExecPath interface { + LookPath(file string) (string, error) +} + +type ExecPath struct { +} + +func (ExecPath) LookPath(file string) (string, error) { + return exec.LookPath(file) +} + +type CmdFactory struct { + execPath IExecPath +} + +func (cmdf CmdFactory) MakeInstallCmd(command string, file string) (*exec.Cmd, error) { + path, err := cmdf.execPath.LookPath(command) + + fileDir := filepath.Dir(file) + + return &exec.Cmd{ + Path: path, + Args: []string{command, "update", + "--no-interaction", // We can't answer any prompts... + "--no-scripts", // Avoid risky scripts + "--ignore-platform-reqs", // We won't run the code, so we don't care about the platform + "--no-autoloader", // We won't execute any code, no need for autoloader + "--no-install", // No need to install packages + "--no-plugins", // We won't run the code, so no plugins needed + "--no-audit", // We don't want to run an audit + }, + Dir: fileDir, + Env: os.Environ(), + }, err +} diff --git a/internal/resolution/pm/composer/cmd_factory_test.go b/internal/resolution/pm/composer/cmd_factory_test.go new file mode 100644 index 00000000..5d5a817a --- /dev/null +++ b/internal/resolution/pm/composer/cmd_factory_test.go @@ -0,0 +1,19 @@ +package composer + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMakeInstallCmd(t *testing.T) { + composerCommand := "composer" + cmd, err := CmdFactory{ + execPath: ExecPath{}, + }.MakeInstallCmd(composerCommand, "file") + assert.NoError(t, err) + assert.NotNil(t, cmd) + args := cmd.Args + assert.Contains(t, args, "composer") + assert.Contains(t, args, "update") +} diff --git a/internal/resolution/pm/composer/job.go b/internal/resolution/pm/composer/job.go new file mode 100644 index 00000000..d61211b5 --- /dev/null +++ b/internal/resolution/pm/composer/job.go @@ -0,0 +1,62 @@ +package composer + +import ( + "github.com/debricked/cli/internal/resolution/job" +) + +const ( + composer = "composer" +) + +type Job struct { + job.BaseJob + install bool + composerCommand string + cmdFactory ICmdFactory +} + +func NewJob( + file string, + install bool, + cmdFactory ICmdFactory, +) *Job { + return &Job{ + BaseJob: job.NewBaseJob(file), + install: install, + cmdFactory: cmdFactory, + } +} + +func (j *Job) Install() bool { + return j.install +} + +func (j *Job) Run() { + if j.install { + + j.SendStatus("installing dependencies") + _, err := j.runInstallCmd() + if err != nil { + j.Errors().Critical(err) + + return + } + } + +} + +func (j *Job) runInstallCmd() ([]byte, error) { + + j.composerCommand = composer + installCmd, err := j.cmdFactory.MakeInstallCmd(j.composerCommand, j.GetFile()) + if err != nil { + return nil, err + } + + installCmdOutput, err := installCmd.Output() + if err != nil { + return nil, j.GetExitError(err) + } + + return installCmdOutput, nil +} diff --git a/internal/resolution/pm/composer/job_test.go b/internal/resolution/pm/composer/job_test.go new file mode 100644 index 00000000..b9a5ee51 --- /dev/null +++ b/internal/resolution/pm/composer/job_test.go @@ -0,0 +1,64 @@ +package composer + +import ( + "errors" + "testing" + + jobTestdata "github.com/debricked/cli/internal/resolution/job/testdata" + "github.com/debricked/cli/internal/resolution/pm/composer/testdata" + "github.com/stretchr/testify/assert" +) + +const ( + badName = "bad-name" +) + +func TestNewJob(t *testing.T) { + j := NewJob("file", false, CmdFactory{ + execPath: ExecPath{}, + }) + assert.Equal(t, "file", j.GetFile()) + assert.False(t, j.Errors().HasError()) +} + +func TestRunInstall(t *testing.T) { + cmdFactoryMock := testdata.NewEchoCmdFactory() + j := NewJob("file", false, cmdFactoryMock) + + _, err := j.runInstallCmd() + assert.NoError(t, err) + + assert.False(t, j.Errors().HasError()) +} + +func TestInstall(t *testing.T) { + j := Job{install: true} + assert.Equal(t, true, j.Install()) + + j = Job{install: false} + assert.Equal(t, false, j.Install()) +} + +func TestRunInstallCmdErr(t *testing.T) { + cmdErr := errors.New("cmd-error") + cmdFactoryMock := testdata.NewEchoCmdFactory() + cmdFactoryMock.MakeInstallErr = cmdErr + j := NewJob("file", true, cmdFactoryMock) + + go jobTestdata.WaitStatus(j) + j.Run() + + assert.Len(t, j.Errors().GetAll(), 1) + assert.Contains(t, j.Errors().GetAll(), cmdErr) +} + +func TestRunInstallCmdOutputErr(t *testing.T) { + cmdMock := testdata.NewEchoCmdFactory() + cmdMock.InstallCmdName = badName + j := NewJob("file", true, cmdMock) + + go jobTestdata.WaitStatus(j) + j.Run() + + jobTestdata.AssertPathErr(t, j.Errors()) +} diff --git a/internal/resolution/pm/composer/pm.go b/internal/resolution/pm/composer/pm.go new file mode 100644 index 00000000..8e8311f0 --- /dev/null +++ b/internal/resolution/pm/composer/pm.go @@ -0,0 +1,23 @@ +package composer + +const Name = "composer" + +type Pm struct { + name string +} + +func NewPm() Pm { + return Pm{ + name: Name, + } +} + +func (pm Pm) Name() string { + return pm.name +} + +func (Pm) Manifests() []string { + return []string{ + `composer\.json$`, + } +} diff --git a/internal/resolution/pm/composer/pm_test.go b/internal/resolution/pm/composer/pm_test.go new file mode 100644 index 00000000..59a3f480 --- /dev/null +++ b/internal/resolution/pm/composer/pm_test.go @@ -0,0 +1,40 @@ +package composer + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewPm(t *testing.T) { + pm := NewPm() + assert.Equal(t, Name, pm.name) +} + +func TestName(t *testing.T) { + pm := NewPm() + assert.Equal(t, Name, pm.Name()) +} + +func TestManifests(t *testing.T) { + pm := Pm{} + manifests := pm.Manifests() + assert.Len(t, manifests, 1) + manifest := manifests[0] + assert.Equal(t, `composer\.json$`, manifest) + _, err := regexp.Compile(manifest) + assert.NoError(t, err) + + cases := map[string]bool{ + "composer.json": true, + "composer.lock": false, + "package-lock.json": false, + } + for file, isMatch := range cases { + t.Run(file, func(t *testing.T) { + matched, _ := regexp.MatchString(manifest, file) + assert.Equal(t, isMatch, matched) + }) + } +} diff --git a/internal/resolution/pm/composer/strategy.go b/internal/resolution/pm/composer/strategy.go new file mode 100644 index 00000000..c17ee9e6 --- /dev/null +++ b/internal/resolution/pm/composer/strategy.go @@ -0,0 +1,29 @@ +package composer + +import ( + "github.com/debricked/cli/internal/resolution/job" +) + +type Strategy struct { + files []string +} + +func (s Strategy) Invoke() ([]job.IJob, error) { + var jobs []job.IJob + for _, file := range s.files { + jobs = append(jobs, NewJob( + file, + true, + CmdFactory{ + execPath: ExecPath{}, + }, + ), + ) + } + + return jobs, nil +} + +func NewStrategy(files []string) Strategy { + return Strategy{files} +} diff --git a/internal/resolution/pm/composer/strategy_test.go b/internal/resolution/pm/composer/strategy_test.go new file mode 100644 index 00000000..d8ba1988 --- /dev/null +++ b/internal/resolution/pm/composer/strategy_test.go @@ -0,0 +1,43 @@ +package composer + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewStrategy(t *testing.T) { + s := NewStrategy(nil) + assert.NotNil(t, s) + assert.Len(t, s.files, 0) + + s = NewStrategy([]string{}) + assert.NotNil(t, s) + assert.Len(t, s.files, 0) + + s = NewStrategy([]string{"file"}) + assert.NotNil(t, s) + assert.Len(t, s.files, 1) + + s = NewStrategy([]string{"file-1", "file-2"}) + assert.NotNil(t, s) + assert.Len(t, s.files, 2) +} + +func TestInvokeNoFiles(t *testing.T) { + s := NewStrategy([]string{}) + jobs, _ := s.Invoke() + assert.Empty(t, jobs) +} + +func TestInvokeOneFile(t *testing.T) { + s := NewStrategy([]string{"file"}) + jobs, _ := s.Invoke() + assert.Len(t, jobs, 1) +} + +func TestInvokeManyFiles(t *testing.T) { + s := NewStrategy([]string{"file-1", "file-2"}) + jobs, _ := s.Invoke() + assert.Len(t, jobs, 2) +} diff --git a/internal/resolution/pm/composer/testdata/cmd_factory_mock.go b/internal/resolution/pm/composer/testdata/cmd_factory_mock.go new file mode 100644 index 00000000..093ba6b2 --- /dev/null +++ b/internal/resolution/pm/composer/testdata/cmd_factory_mock.go @@ -0,0 +1,20 @@ +package testdata + +import ( + "os/exec" +) + +type CmdFactoryMock struct { + InstallCmdName string + MakeInstallErr error +} + +func NewEchoCmdFactory() CmdFactoryMock { + return CmdFactoryMock{ + InstallCmdName: "echo", + } +} + +func (f CmdFactoryMock) MakeInstallCmd(command string, file string) (*exec.Cmd, error) { + return exec.Command(f.InstallCmdName), f.MakeInstallErr +} diff --git a/internal/resolution/pm/pm.go b/internal/resolution/pm/pm.go index a0860ebd..d9503c85 100644 --- a/internal/resolution/pm/pm.go +++ b/internal/resolution/pm/pm.go @@ -1,6 +1,7 @@ package pm import ( + "github.com/debricked/cli/internal/resolution/pm/composer" "github.com/debricked/cli/internal/resolution/pm/gomod" "github.com/debricked/cli/internal/resolution/pm/gradle" "github.com/debricked/cli/internal/resolution/pm/maven" @@ -22,5 +23,6 @@ func Pms() []IPm { pip.NewPm(), yarn.NewPm(), nuget.NewPm(), + composer.NewPm(), } } diff --git a/internal/resolution/pm/pm_test.go b/internal/resolution/pm/pm_test.go index 5dd4eee0..aabd4f55 100644 --- a/internal/resolution/pm/pm_test.go +++ b/internal/resolution/pm/pm_test.go @@ -12,6 +12,7 @@ func TestPms(t *testing.T) { "mvn", "go", "gradle", + "composer", } for _, pmName := range pmNames { diff --git a/internal/resolution/strategy/strategy_factory.go b/internal/resolution/strategy/strategy_factory.go index 40c95926..5274e7b3 100644 --- a/internal/resolution/strategy/strategy_factory.go +++ b/internal/resolution/strategy/strategy_factory.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/debricked/cli/internal/resolution/file" + "github.com/debricked/cli/internal/resolution/pm/composer" "github.com/debricked/cli/internal/resolution/pm/gomod" "github.com/debricked/cli/internal/resolution/pm/gradle" "github.com/debricked/cli/internal/resolution/pm/maven" @@ -37,6 +38,8 @@ func (sf Factory) Make(pmFileBatch file.IBatch, paths []string) (IStrategy, erro return yarn.NewStrategy(pmFileBatch.Files()), nil case nuget.Name: return nuget.NewStrategy(pmFileBatch.Files()), nil + case composer.Name: + return composer.NewStrategy(pmFileBatch.Files()), nil default: return nil, fmt.Errorf("failed to make strategy from %s", name) } diff --git a/internal/resolution/strategy/strategy_factory_test.go b/internal/resolution/strategy/strategy_factory_test.go index 1eeca37f..492bb0c7 100644 --- a/internal/resolution/strategy/strategy_factory_test.go +++ b/internal/resolution/strategy/strategy_factory_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/debricked/cli/internal/resolution/file" + "github.com/debricked/cli/internal/resolution/pm/composer" "github.com/debricked/cli/internal/resolution/pm/gomod" "github.com/debricked/cli/internal/resolution/pm/gradle" "github.com/debricked/cli/internal/resolution/pm/maven" @@ -29,12 +30,13 @@ func TestMakeErr(t *testing.T) { func TestMake(t *testing.T) { cases := map[string]IStrategy{ - maven.Name: maven.NewStrategy(nil), - gradle.Name: gradle.NewStrategy(nil, nil), - gomod.Name: gomod.NewStrategy(nil), - pip.Name: pip.NewStrategy(nil), - yarn.Name: yarn.NewStrategy(nil), - nuget.Name: nuget.NewStrategy(nil), + maven.Name: maven.NewStrategy(nil), + gradle.Name: gradle.NewStrategy(nil, nil), + gomod.Name: gomod.NewStrategy(nil), + pip.Name: pip.NewStrategy(nil), + yarn.Name: yarn.NewStrategy(nil), + nuget.Name: nuget.NewStrategy(nil), + composer.Name: composer.NewStrategy(nil), } f := NewStrategyFactory() var batch file.IBatch diff --git a/test/resolve/resolver_test.go b/test/resolve/resolver_test.go index 81ddaeb0..9baffda1 100644 --- a/test/resolve/resolver_test.go +++ b/test/resolve/resolver_test.go @@ -17,6 +17,12 @@ func TestResolves(t *testing.T) { lockFileName string packageManager string }{ + { + name: "basic composer.json", + manifestFile: "testdata/composer/composer.json", + lockFileName: "composer.lock", + packageManager: "composer", + }, { name: "basic package.json", manifestFile: "testdata/npm/package.json", diff --git a/test/resolve/testdata/composer/composer.json b/test/resolve/testdata/composer/composer.json new file mode 100644 index 00000000..15d3236f --- /dev/null +++ b/test/resolve/testdata/composer/composer.json @@ -0,0 +1,14 @@ +{ + "name": "debricked/test", + "require": { + "php": ">=8.2.0", + "ext-curl": "*", + "ext-dom": "*", + "doctrine/annotations": "^2.0", + "justinrainbow/json-schema": "^5.2", + "psr/log": "^1.0.1" + }, + "require-dev": { + "phpstan/phpstan": "^1.4.3" + } +}