diff --git a/cmpserver/plugin/plugin.go b/cmpserver/plugin/plugin.go index 3990b0fa2796d..909d420acb371 100644 --- a/cmpserver/plugin/plugin.go +++ b/cmpserver/plugin/plugin.go @@ -46,8 +46,7 @@ func NewService(initConstants CMPServerInitConstants) *Service { } } -func (s *Service) Init() error { - workDir := common.GetCMPWorkDir() +func (s *Service) Init(workDir string) error { err := os.RemoveAll(workDir) if err != nil { return fmt.Errorf("error removing workdir %q: %s", workDir, err) diff --git a/cmpserver/plugin/plugin_test.go b/cmpserver/plugin/plugin_test.go index b3940aa0256ec..c796cb8c80e86 100644 --- a/cmpserver/plugin/plugin_test.go +++ b/cmpserver/plugin/plugin_test.go @@ -3,6 +3,7 @@ package plugin import ( "context" "os" + "path" "path/filepath" "testing" "time" @@ -12,6 +13,7 @@ import ( "gopkg.in/yaml.v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/argoproj/argo-cd/v2/cmpserver/apiclient" repoclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient" "github.com/argoproj/argo-cd/v2/test" ) @@ -32,6 +34,11 @@ func newService(configFilePath string) (*Service, error) { return service, nil } +func (s *Service) WithGenerateCommand(command Command) *Service { + s.initConstants.PluginConfig.Spec.Generate = command + return s +} + type pluginOpt func(*CMPServerInitConstants) func withDiscover(d Discover) pluginOpt { @@ -104,6 +111,19 @@ func TestMatchRepository(t *testing.T) { assert.NoError(t, err) assert.False(t, match) }) + t.Run("will not match a pattern with a syntax error", func(t *testing.T) { + // given + d := Discover{ + FileName: "[", + } + f := setup(t, withDiscover(d)) + + // when + _, err := f.service.matchRepository(context.Background(), f.path) + + // then + assert.ErrorContains(t, err, "syntax error") + }) t.Run("will match plugin by glob", func(t *testing.T) { // given d := Discover{ @@ -136,6 +156,21 @@ func TestMatchRepository(t *testing.T) { assert.NoError(t, err) assert.False(t, match) }) + t.Run("will throw an error for a bad pattern", func(t *testing.T) { + // given + d := Discover{ + Find: Find{ + Glob: "does-not-exist", + }, + } + f := setup(t, withDiscover(d)) + + // when + _, err := f.service.matchRepository(context.Background(), f.path) + + // then + assert.ErrorContains(t, err, "error finding glob match for pattern") + }) t.Run("will match plugin by command when returns any output", func(t *testing.T) { // given d := Discover{ @@ -201,17 +236,49 @@ func Test_Negative_ConfigFile_DoesnotExist(t *testing.T) { func TestGenerateManifest(t *testing.T) { configFilePath := "./testdata/kustomize/config" - service, err := newService(configFilePath) - require.NoError(t, err) - res1, err := service.generateManifest(context.Background(), "", nil) + t.Run("successful generate", func(t *testing.T) { + service, err := newService(configFilePath) + require.NoError(t, err) + + res1, err := service.generateManifest(context.Background(), "", nil) + require.NoError(t, err) + require.NotNil(t, res1) + + expectedOutput := "{\"apiVersion\":\"v1\",\"data\":{\"foo\":\"bar\"},\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}" + if res1 != nil { + require.Equal(t, expectedOutput, res1.Manifests[0]) + } + }) + t.Run("bad generate command", func(t *testing.T) { + service, err := newService(configFilePath) + require.NoError(t, err) + service.WithGenerateCommand(Command{Command: []string{"bad-command"}}) + + res, err := service.generateManifest(context.Background(), "", nil) + assert.ErrorContains(t, err, "executable file not found") + assert.Nil(t, res.Manifests) + }) + t.Run("bad yaml output", func(t *testing.T) { + service, err := newService(configFilePath) + require.NoError(t, err) + service.WithGenerateCommand(Command{Command: []string{"echo", "invalid yaml: }"}}) + + res, err := service.generateManifest(context.Background(), "", nil) + assert.ErrorContains(t, err, "failed to unmarshal manifest") + assert.Nil(t, res.Manifests) + }) +} + +func TestGenerateManifest_deadline_exceeded(t *testing.T) { + configFilePath := "./testdata/kustomize/config" + service, err := newService(configFilePath) require.NoError(t, err) - require.NotNil(t, res1) - expectedOutput := "{\"apiVersion\":\"v1\",\"data\":{\"foo\":\"bar\"},\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}" - if res1 != nil { - require.Equal(t, expectedOutput, res1.Manifests[0]) - } + expiredCtx, cancel := context.WithTimeout(context.Background(), time.Second * 0) + defer cancel() + _, err = service.generateManifest(expiredCtx, "", nil) + assert.ErrorContains(t, err, "context deadline exceeded") } // TestRunCommandContextTimeout makes sure the command dies at timeout rather than sleeping past the timeout. @@ -230,6 +297,11 @@ func TestRunCommandContextTimeout(t *testing.T) { assert.Less(t, after.Sub(before), 1*time.Second) } +func TestRunCommandEmptyCommand(t *testing.T) { + _, err := runCommand(context.Background(), Command{}, "", nil) + assert.ErrorContains(t, err, "Command is empty") +} + func Test_getParametersAnnouncement_empty_command(t *testing.T) { staticYAML := ` - name: static-a @@ -306,6 +378,15 @@ func Test_getParametersAnnouncement_bad_command(t *testing.T) { func Test_getTempDirMustCleanup(t *testing.T) { tempDir := t.TempDir() + + // Induce a directory create error to verify error handling. + err := os.Chmod(tempDir, 0000) + require.NoError(t, err) + _, _, err = getTempDirMustCleanup(path.Join(tempDir, "test")) + assert.ErrorContains(t, err, "error creating temp dir") + + err = os.Chmod(tempDir, 0700) + require.NoError(t, err) workDir, cleanup, err := getTempDirMustCleanup(tempDir) require.NoError(t, err) require.DirExists(t, workDir) @@ -322,3 +403,52 @@ func Test_getTempDirMustCleanup(t *testing.T) { cleanup() assert.NoDirExists(t, workDir) } + +func TestService_Init(t *testing.T) { + // Set up a base directory containing a test directory and a test file. + tempDir := t.TempDir() + workDir := path.Join(tempDir, "workDir") + err := os.MkdirAll(workDir, 0700) + require.NoError(t, err) + testfile := path.Join(workDir, "testfile") + file, err := os.Create(testfile) + require.NoError(t, err) + err = file.Close() + require.NoError(t, err) + + // Make the base directory read-only so Init's cleanup fails. + err = os.Chmod(tempDir, 0000) + require.NoError(t, err) + s := NewService(CMPServerInitConstants{PluginConfig: PluginConfig{}}) + err = s.Init(workDir) + assert.ErrorContains(t, err, "error removing workdir", "Init must throw an error if it can't remove the work directory") + + // Make the base directory writable so Init's cleanup succeeds. + err = os.Chmod(tempDir, 0700) + require.NoError(t, err) + err = s.Init(workDir) + assert.NoError(t, err) + assert.DirExists(t, workDir) + assert.NoFileExists(t, testfile) +} + +func TestEnviron(t *testing.T) { + t.Run("empty environ", func(t *testing.T) { + env := environ([]*apiclient.EnvEntry{}) + assert.Nil(t, env) + }) + t.Run("env vars with empty names or values", func(t *testing.T) { + env := environ([]*apiclient.EnvEntry{ + {Value: "test"}, + {Name: "test"}, + }) + assert.Nil(t, env) + }) + t.Run("proper env vars", func(t *testing.T) { + env := environ([]*apiclient.EnvEntry{ + {Name: "name1", Value: "value1"}, + {Name: "name2", Value: "value2"}, + }) + assert.Equal(t, []string{"name1=value1", "name2=value2"}, env) + }) +} diff --git a/cmpserver/server.go b/cmpserver/server.go index 636dbcd1f7339..635f8771b4f78 100644 --- a/cmpserver/server.go +++ b/cmpserver/server.go @@ -2,12 +2,13 @@ package cmpserver import ( "fmt" - "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "net" "os" "os/signal" "syscall" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" @@ -101,7 +102,7 @@ func (a *ArgoCDCMPServer) CreateGRPC() (*grpc.Server, error) { return true, nil })) pluginService := plugin.NewService(a.initConstants) - err := pluginService.Init() + err := pluginService.Init(common.GetCMPWorkDir()) if err != nil { return nil, fmt.Errorf("error initializing plugin service: %s", err) }