github.com/argoproj/argo-cd/v3@v3.2.1/cmpserver/plugin/plugin_test.go (about)

     1  package plugin
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/golang/protobuf/ptypes/empty"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  	"google.golang.org/grpc/metadata"
    18  	"gopkg.in/yaml.v2"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  
    21  	"github.com/argoproj/argo-cd/v3/cmpserver/apiclient"
    22  	repoclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    23  	"github.com/argoproj/argo-cd/v3/test"
    24  	"github.com/argoproj/argo-cd/v3/util/cmp"
    25  	"github.com/argoproj/argo-cd/v3/util/tgzstream"
    26  )
    27  
    28  func newService(configFilePath string) (*Service, error) {
    29  	config, err := ReadPluginConfig(configFilePath)
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  
    34  	initConstants := CMPServerInitConstants{
    35  		PluginConfig: *config,
    36  	}
    37  
    38  	service := &Service{
    39  		initConstants: initConstants,
    40  	}
    41  	return service, nil
    42  }
    43  
    44  func (s *Service) WithGenerateCommand(command Command) *Service {
    45  	s.initConstants.PluginConfig.Spec.Generate = command
    46  	return s
    47  }
    48  
    49  type pluginOpt func(*CMPServerInitConstants)
    50  
    51  func withDiscover(d Discover) pluginOpt {
    52  	return func(cic *CMPServerInitConstants) {
    53  		cic.PluginConfig.Spec.Discover = d
    54  	}
    55  }
    56  
    57  func buildPluginConfig(opts ...pluginOpt) *CMPServerInitConstants {
    58  	cic := &CMPServerInitConstants{
    59  		PluginConfig: PluginConfig{
    60  			TypeMeta: metav1.TypeMeta{
    61  				Kind:       "ConfigManagementPlugin",
    62  				APIVersion: "argoproj.io/v1alpha1",
    63  			},
    64  			Metadata: metav1.ObjectMeta{
    65  				Name: "some-plugin",
    66  			},
    67  			Spec: PluginConfigSpec{
    68  				Version: "v1.0",
    69  			},
    70  		},
    71  	}
    72  	for _, opt := range opts {
    73  		opt(cic)
    74  	}
    75  	return cic
    76  }
    77  
    78  func TestMatchRepository(t *testing.T) {
    79  	type fixture struct {
    80  		service *Service
    81  		path    string
    82  		env     []*apiclient.EnvEntry
    83  	}
    84  	setup := func(t *testing.T, opts ...pluginOpt) *fixture {
    85  		t.Helper()
    86  		cic := buildPluginConfig(opts...)
    87  		path := filepath.Join(test.GetTestDir(t), "testdata", "kustomize")
    88  		s := NewService(*cic)
    89  		return &fixture{
    90  			service: s,
    91  			path:    path,
    92  			env:     []*apiclient.EnvEntry{{Name: "ENV_VAR", Value: "1"}},
    93  		}
    94  	}
    95  	t.Run("will match plugin by filename", func(t *testing.T) {
    96  		// given
    97  		d := Discover{
    98  			FileName: "kustomization.yaml",
    99  		}
   100  		f := setup(t, withDiscover(d))
   101  
   102  		// when
   103  		match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   104  
   105  		// then
   106  		require.NoError(t, err)
   107  		assert.True(t, match)
   108  		assert.True(t, discovery)
   109  	})
   110  	t.Run("will not match plugin by filename if file not found", func(t *testing.T) {
   111  		// given
   112  		d := Discover{
   113  			FileName: "not_found.yaml",
   114  		}
   115  		f := setup(t, withDiscover(d))
   116  
   117  		// when
   118  		match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   119  
   120  		// then
   121  		require.NoError(t, err)
   122  		assert.False(t, match)
   123  		assert.True(t, discovery)
   124  	})
   125  	t.Run("will not match a pattern with a syntax error", func(t *testing.T) {
   126  		// given
   127  		d := Discover{
   128  			FileName: "[",
   129  		}
   130  		f := setup(t, withDiscover(d))
   131  
   132  		// when
   133  		_, _, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   134  
   135  		// then
   136  		require.ErrorContains(t, err, "syntax error")
   137  	})
   138  	t.Run("will match plugin by glob", func(t *testing.T) {
   139  		// given
   140  		d := Discover{
   141  			Find: Find{
   142  				Glob: "**/*/plugin.yaml",
   143  			},
   144  		}
   145  		f := setup(t, withDiscover(d))
   146  
   147  		// when
   148  		match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   149  
   150  		// then
   151  		require.NoError(t, err)
   152  		assert.True(t, match)
   153  		assert.True(t, discovery)
   154  	})
   155  	t.Run("will not match plugin by glob if not found", func(t *testing.T) {
   156  		// given
   157  		d := Discover{
   158  			Find: Find{
   159  				Glob: "**/*/not_found.yaml",
   160  			},
   161  		}
   162  		f := setup(t, withDiscover(d))
   163  
   164  		// when
   165  		match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   166  
   167  		// then
   168  		require.NoError(t, err)
   169  		assert.False(t, match)
   170  		assert.True(t, discovery)
   171  	})
   172  	t.Run("will throw an error for a bad pattern", func(t *testing.T) {
   173  		// given
   174  		d := Discover{
   175  			Find: Find{
   176  				Glob: "does-not-exist",
   177  			},
   178  		}
   179  		f := setup(t, withDiscover(d))
   180  
   181  		// when
   182  		_, _, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   183  
   184  		// then
   185  		require.ErrorContains(t, err, "error finding glob match for pattern")
   186  	})
   187  	t.Run("will match plugin by command when returns any output", func(t *testing.T) {
   188  		// given
   189  		d := Discover{
   190  			Find: Find{
   191  				Command: Command{
   192  					Command: []string{"echo", "test"},
   193  				},
   194  			},
   195  		}
   196  		f := setup(t, withDiscover(d))
   197  
   198  		// when
   199  		match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   200  
   201  		// then
   202  		require.NoError(t, err)
   203  		assert.True(t, match)
   204  		assert.True(t, discovery)
   205  	})
   206  	t.Run("will not match plugin by command when returns no output", func(t *testing.T) {
   207  		// given
   208  		d := Discover{
   209  			Find: Find{
   210  				Command: Command{
   211  					Command: []string{"echo"},
   212  				},
   213  			},
   214  		}
   215  		f := setup(t, withDiscover(d))
   216  
   217  		// when
   218  		match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   219  		// then
   220  		require.NoError(t, err)
   221  		assert.False(t, match)
   222  		assert.True(t, discovery)
   223  	})
   224  	t.Run("will match plugin because env var defined", func(t *testing.T) {
   225  		// given
   226  		d := Discover{
   227  			Find: Find{
   228  				Command: Command{
   229  					Command: []string{"sh", "-c", "echo -n $ENV_VAR"},
   230  				},
   231  			},
   232  		}
   233  		f := setup(t, withDiscover(d))
   234  
   235  		// when
   236  		match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   237  
   238  		// then
   239  		require.NoError(t, err)
   240  		assert.True(t, match)
   241  		assert.True(t, discovery)
   242  	})
   243  	t.Run("will not match plugin because no env var defined", func(t *testing.T) {
   244  		// given
   245  		d := Discover{
   246  			Find: Find{
   247  				Command: Command{
   248  					// Use printf instead of echo since OSX prints the "-n" when there's no additional arg.
   249  					Command: []string{"sh", "-c", `printf "%s" "$ENV_NO_VAR"`},
   250  				},
   251  			},
   252  		}
   253  		f := setup(t, withDiscover(d))
   254  
   255  		// when
   256  		match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   257  
   258  		// then
   259  		require.NoError(t, err)
   260  		assert.False(t, match)
   261  		assert.True(t, discovery)
   262  	})
   263  	t.Run("will not match plugin by command when command fails", func(t *testing.T) {
   264  		// given
   265  		d := Discover{
   266  			Find: Find{
   267  				Command: Command{
   268  					Command: []string{"cat", "nil"},
   269  				},
   270  			},
   271  		}
   272  		f := setup(t, withDiscover(d))
   273  
   274  		// when
   275  		match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   276  
   277  		// then
   278  		require.Error(t, err)
   279  		assert.False(t, match)
   280  		assert.True(t, discovery)
   281  	})
   282  	t.Run("will not match plugin as discovery is not set", func(t *testing.T) {
   283  		// given
   284  		d := Discover{}
   285  		f := setup(t, withDiscover(d))
   286  
   287  		// when
   288  		match, discovery, err := f.service.matchRepository(t.Context(), f.path, f.env, ".")
   289  
   290  		// then
   291  		require.NoError(t, err)
   292  		assert.False(t, match)
   293  		assert.False(t, discovery)
   294  	})
   295  }
   296  
   297  func Test_Negative_ConfigFile_DoesnotExist(t *testing.T) {
   298  	configFilePath := "./testdata/kustomize-neg/config"
   299  	service, err := newService(configFilePath)
   300  	require.Error(t, err)
   301  	require.Nil(t, service)
   302  }
   303  
   304  func TestGenerateManifest(t *testing.T) {
   305  	configFilePath := "./testdata/kustomize/config"
   306  
   307  	t.Run("successful generate", func(t *testing.T) {
   308  		service, err := newService(configFilePath)
   309  		require.NoError(t, err)
   310  
   311  		res1, err := service.generateManifest(t.Context(), "testdata/kustomize", nil)
   312  		require.NoError(t, err)
   313  		require.NotNil(t, res1)
   314  
   315  		expectedOutput := "{\"apiVersion\":\"v1\",\"data\":{\"foo\":\"bar\"},\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"
   316  		if res1 != nil {
   317  			require.Equal(t, expectedOutput, res1.Manifests[0])
   318  		}
   319  	})
   320  	t.Run("bad generate command", func(t *testing.T) {
   321  		service, err := newService(configFilePath)
   322  		require.NoError(t, err)
   323  		service.WithGenerateCommand(Command{Command: []string{"bad-command"}})
   324  
   325  		res, err := service.generateManifest(t.Context(), "testdata/kustomize", nil)
   326  		require.ErrorContains(t, err, "executable file not found")
   327  		assert.Nil(t, res.Manifests)
   328  	})
   329  	t.Run("bad yaml output", func(t *testing.T) {
   330  		service, err := newService(configFilePath)
   331  		require.NoError(t, err)
   332  		service.WithGenerateCommand(Command{Command: []string{"echo", "invalid yaml: }"}})
   333  
   334  		res, err := service.generateManifest(t.Context(), "testdata/kustomize", nil)
   335  		require.ErrorContains(t, err, "failed to unmarshal manifest")
   336  		assert.Nil(t, res.Manifests)
   337  	})
   338  }
   339  
   340  func TestGenerateManifest_deadline_exceeded(t *testing.T) {
   341  	configFilePath := "./testdata/kustomize/config"
   342  	service, err := newService(configFilePath)
   343  	require.NoError(t, err)
   344  
   345  	expiredCtx, cancel := context.WithTimeout(t.Context(), time.Second*0)
   346  	defer cancel()
   347  	_, err = service.generateManifest(expiredCtx, "", nil)
   348  	require.ErrorContains(t, err, "context deadline exceeded")
   349  }
   350  
   351  // TestRunCommandContextTimeout makes sure the command dies at timeout rather than sleeping past the timeout.
   352  func TestRunCommandContextTimeout(t *testing.T) {
   353  	ctx, cancel := context.WithTimeout(t.Context(), 990*time.Millisecond)
   354  	defer cancel()
   355  	// Use a subshell so there's a child command.
   356  	command := Command{
   357  		Command: []string{"sh", "-c"},
   358  		Args:    []string{"sleep 5"},
   359  	}
   360  	before := time.Now()
   361  	_, err := runCommand(ctx, command, "", []string{})
   362  	after := time.Now()
   363  	require.Error(t, err) // The command should time out, causing an error.
   364  	assert.Less(t, after.Sub(before), 1*time.Second)
   365  }
   366  
   367  func TestRunCommandEmptyCommand(t *testing.T) {
   368  	_, err := runCommand(t.Context(), Command{}, "", nil)
   369  	require.ErrorContains(t, err, "Command is empty")
   370  }
   371  
   372  // TestRunCommandContextTimeoutWithCleanup makes sure that the process is given enough time to cleanup before sending SIGKILL.
   373  func TestRunCommandContextTimeoutWithCleanup(t *testing.T) {
   374  	ctx, cancel := context.WithTimeout(t.Context(), 900*time.Millisecond)
   375  	defer cancel()
   376  
   377  	// Use a subshell so there's a child command.
   378  	// This command sleeps for 4 seconds which is currently less than the 5 second delay between SIGTERM and SIGKILL signal and then exits successfully.
   379  	command := Command{
   380  		Command: []string{"sh", "-c"},
   381  		Args:    []string{`(trap 'echo "cleanup completed"; exit' TERM; sleep 4)`},
   382  	}
   383  
   384  	before := time.Now()
   385  	output, err := runCommand(ctx, command, "", []string{})
   386  	after := time.Now()
   387  
   388  	require.Error(t, err) // The command should time out, causing an error.
   389  	assert.Less(t, after.Sub(before), 1*time.Second)
   390  	// The command should still have completed the cleanup after termination.
   391  	assert.Contains(t, output, "cleanup completed")
   392  }
   393  
   394  func Test_getParametersAnnouncement_empty_command(t *testing.T) {
   395  	staticYAML := `
   396  - name: static-a
   397  - name: static-b
   398  `
   399  	static := &[]*repoclient.ParameterAnnouncement{}
   400  	err := yaml.Unmarshal([]byte(staticYAML), static)
   401  	require.NoError(t, err)
   402  	command := Command{
   403  		Command: []string{"echo"},
   404  		Args:    []string{`[]`},
   405  	}
   406  	res, err := getParametersAnnouncement(t.Context(), "", *static, command, []*apiclient.EnvEntry{})
   407  	require.NoError(t, err)
   408  	assert.Equal(t, []*repoclient.ParameterAnnouncement{{Name: "static-a"}, {Name: "static-b"}}, res.ParameterAnnouncements)
   409  }
   410  
   411  func Test_getParametersAnnouncement_no_command(t *testing.T) {
   412  	staticYAML := `
   413  - name: static-a
   414  - name: static-b
   415  `
   416  	static := &[]*repoclient.ParameterAnnouncement{}
   417  	err := yaml.Unmarshal([]byte(staticYAML), static)
   418  	require.NoError(t, err)
   419  	command := Command{}
   420  	res, err := getParametersAnnouncement(t.Context(), "", *static, command, []*apiclient.EnvEntry{})
   421  	require.NoError(t, err)
   422  	assert.Equal(t, []*repoclient.ParameterAnnouncement{{Name: "static-a"}, {Name: "static-b"}}, res.ParameterAnnouncements)
   423  }
   424  
   425  func Test_getParametersAnnouncement_static_and_dynamic(t *testing.T) {
   426  	staticYAML := `
   427  - name: static-a
   428  - name: static-b
   429  `
   430  	static := &[]*repoclient.ParameterAnnouncement{}
   431  	err := yaml.Unmarshal([]byte(staticYAML), static)
   432  	require.NoError(t, err)
   433  	command := Command{
   434  		Command: []string{"echo"},
   435  		Args:    []string{`[{"name": "dynamic-a"}, {"name": "dynamic-b"}]`},
   436  	}
   437  	res, err := getParametersAnnouncement(t.Context(), "", *static, command, []*apiclient.EnvEntry{})
   438  	require.NoError(t, err)
   439  	expected := []*repoclient.ParameterAnnouncement{
   440  		{Name: "dynamic-a"},
   441  		{Name: "dynamic-b"},
   442  		{Name: "static-a"},
   443  		{Name: "static-b"},
   444  	}
   445  	assert.Equal(t, expected, res.ParameterAnnouncements)
   446  }
   447  
   448  func Test_getParametersAnnouncement_invalid_json(t *testing.T) {
   449  	command := Command{
   450  		Command: []string{"echo"},
   451  		Args:    []string{`[`},
   452  	}
   453  	_, err := getParametersAnnouncement(t.Context(), "", []*repoclient.ParameterAnnouncement{}, command, []*apiclient.EnvEntry{})
   454  	assert.ErrorContains(t, err, "unexpected end of JSON input")
   455  }
   456  
   457  func Test_getParametersAnnouncement_bad_command(t *testing.T) {
   458  	command := Command{
   459  		Command: []string{"exit"},
   460  		Args:    []string{"1"},
   461  	}
   462  	_, err := getParametersAnnouncement(t.Context(), "", []*repoclient.ParameterAnnouncement{}, command, []*apiclient.EnvEntry{})
   463  	assert.ErrorContains(t, err, "error executing dynamic parameter output command")
   464  }
   465  
   466  func Test_getTempDirMustCleanup(t *testing.T) {
   467  	tempDir := t.TempDir()
   468  
   469  	// Induce a directory create error to verify error handling.
   470  	err := os.Chmod(tempDir, 0o000)
   471  	require.NoError(t, err)
   472  	_, _, err = getTempDirMustCleanup(path.Join(tempDir, "test"))
   473  	require.ErrorContains(t, err, "error creating temp dir")
   474  
   475  	err = os.Chmod(tempDir, 0o700)
   476  	require.NoError(t, err)
   477  	workDir, cleanup, err := getTempDirMustCleanup(tempDir)
   478  	require.NoError(t, err)
   479  	require.DirExists(t, workDir)
   480  	cleanup()
   481  	assert.NoDirExists(t, workDir)
   482  }
   483  
   484  func TestService_Init(t *testing.T) {
   485  	// Set up a base directory containing a test directory and a test file.
   486  	tempDir := t.TempDir()
   487  	workDir := path.Join(tempDir, "workDir")
   488  	err := os.MkdirAll(workDir, 0o700)
   489  	require.NoError(t, err)
   490  	testfile := path.Join(workDir, "testfile")
   491  	file, err := os.Create(testfile)
   492  	require.NoError(t, err)
   493  	err = file.Close()
   494  	require.NoError(t, err)
   495  
   496  	// Make the base directory read-only so Init's cleanup fails.
   497  	err = os.Chmod(tempDir, 0o000)
   498  	require.NoError(t, err)
   499  	s := NewService(CMPServerInitConstants{PluginConfig: PluginConfig{}})
   500  	err = s.Init(workDir)
   501  	require.ErrorContains(t, err, "error removing workdir", "Init must throw an error if it can't remove the work directory")
   502  
   503  	// Make the base directory writable so Init's cleanup succeeds.
   504  	err = os.Chmod(tempDir, 0o700)
   505  	require.NoError(t, err)
   506  	err = s.Init(workDir)
   507  	require.NoError(t, err)
   508  	assert.DirExists(t, workDir)
   509  	assert.NoFileExists(t, testfile)
   510  }
   511  
   512  func TestEnviron(t *testing.T) {
   513  	t.Run("empty environ", func(t *testing.T) {
   514  		env := environ([]*apiclient.EnvEntry{})
   515  		assert.Nil(t, env)
   516  	})
   517  	t.Run("env vars with empty names", func(t *testing.T) {
   518  		env := environ([]*apiclient.EnvEntry{
   519  			{Value: "test"},
   520  			{Name: "test"},
   521  		})
   522  		assert.Equal(t, []string{"test="}, env)
   523  	})
   524  	t.Run("proper env vars", func(t *testing.T) {
   525  		env := environ([]*apiclient.EnvEntry{
   526  			{Name: "name1", Value: "value1"},
   527  			{Name: "name2", Value: "value2"},
   528  			{Name: "name3", Value: ""},
   529  		})
   530  		assert.Equal(t, []string{"name1=value1", "name2=value2", "name3="}, env)
   531  	})
   532  }
   533  
   534  func TestIsDiscoveryConfigured(t *testing.T) {
   535  	type fixture struct {
   536  		service *Service
   537  	}
   538  	setup := func(t *testing.T, opts ...pluginOpt) *fixture {
   539  		t.Helper()
   540  		cic := buildPluginConfig(opts...)
   541  		s := NewService(*cic)
   542  		return &fixture{
   543  			service: s,
   544  		}
   545  	}
   546  	t.Run("discovery is enabled when is configured by FileName", func(t *testing.T) {
   547  		// given
   548  		d := Discover{
   549  			FileName: "kustomization.yaml",
   550  		}
   551  		f := setup(t, withDiscover(d))
   552  
   553  		// when
   554  		isDiscoveryConfigured := f.service.isDiscoveryConfigured()
   555  
   556  		// then
   557  		assert.True(t, isDiscoveryConfigured)
   558  	})
   559  	t.Run("discovery is enabled when is configured by Glob", func(t *testing.T) {
   560  		// given
   561  		d := Discover{
   562  			Find: Find{
   563  				Glob: "**/*/plugin.yaml",
   564  			},
   565  		}
   566  		f := setup(t, withDiscover(d))
   567  
   568  		// when
   569  		isDiscoveryConfigured := f.service.isDiscoveryConfigured()
   570  
   571  		// then
   572  		assert.True(t, isDiscoveryConfigured)
   573  	})
   574  	t.Run("discovery is enabled when is configured by Command", func(t *testing.T) {
   575  		// given
   576  		d := Discover{
   577  			Find: Find{
   578  				Command: Command{
   579  					Command: []string{"echo", "test"},
   580  				},
   581  			},
   582  		}
   583  		f := setup(t, withDiscover(d))
   584  
   585  		// when
   586  		isDiscoveryConfigured := f.service.isDiscoveryConfigured()
   587  
   588  		// then
   589  		assert.True(t, isDiscoveryConfigured)
   590  	})
   591  	t.Run("discovery is disabled when discover is not configured", func(t *testing.T) {
   592  		// given
   593  		d := Discover{}
   594  		f := setup(t, withDiscover(d))
   595  
   596  		// when
   597  		isDiscoveryConfigured := f.service.isDiscoveryConfigured()
   598  
   599  		// then
   600  		assert.False(t, isDiscoveryConfigured)
   601  	})
   602  }
   603  
   604  type MockGenerateManifestStream struct {
   605  	metadataSent    bool
   606  	fileSent        bool
   607  	metadataRequest *apiclient.AppStreamRequest
   608  	fileRequest     *apiclient.AppStreamRequest
   609  	response        *apiclient.ManifestResponse
   610  }
   611  
   612  func NewMockGenerateManifestStream(repoPath, appPath string, env []string) (*MockGenerateManifestStream, error) {
   613  	tgz, mr, err := cmp.GetCompressedRepoAndMetadata(repoPath, appPath, env, nil, nil)
   614  	if err != nil {
   615  		return nil, err
   616  	}
   617  	defer tgzstream.CloseAndDelete(tgz)
   618  
   619  	tgzBuffer := bytes.NewBuffer(nil)
   620  	_, err = io.Copy(tgzBuffer, tgz)
   621  	if err != nil {
   622  		return nil, fmt.Errorf("failed to copy manifest targz to a byte buffer: %w", err)
   623  	}
   624  
   625  	return &MockGenerateManifestStream{
   626  		metadataRequest: mr,
   627  		fileRequest:     cmp.AppFileRequest(tgzBuffer.Bytes()),
   628  	}, nil
   629  }
   630  
   631  func (m *MockGenerateManifestStream) SendAndClose(response *apiclient.ManifestResponse) error {
   632  	m.response = response
   633  	return nil
   634  }
   635  
   636  func (m *MockGenerateManifestStream) Recv() (*apiclient.AppStreamRequest, error) {
   637  	if !m.metadataSent {
   638  		m.metadataSent = true
   639  		return m.metadataRequest, nil
   640  	}
   641  
   642  	if !m.fileSent {
   643  		m.fileSent = true
   644  		return m.fileRequest, nil
   645  	}
   646  	return nil, io.EOF
   647  }
   648  
   649  func (m *MockGenerateManifestStream) Context() context.Context {
   650  	return context.Background()
   651  }
   652  
   653  func TestService_GenerateManifest(t *testing.T) {
   654  	configFilePath := "./testdata/kustomize/config"
   655  	service, err := newService(configFilePath)
   656  	require.NoError(t, err)
   657  
   658  	t.Run("successful generate", func(t *testing.T) {
   659  		s, err := NewMockGenerateManifestStream("./testdata/kustomize", "./testdata/kustomize", nil)
   660  		require.NoError(t, err)
   661  		err = service.generateManifestGeneric(s)
   662  		require.NoError(t, err)
   663  		require.NotNil(t, s.response)
   664  		assert.Equal(t, []string{"{\"apiVersion\":\"v1\",\"data\":{\"foo\":\"bar\"},\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"}, s.response.Manifests)
   665  	})
   666  
   667  	t.Run("out-of-bounds app path", func(t *testing.T) {
   668  		s, err := NewMockGenerateManifestStream("./testdata/kustomize", "./testdata/kustomize", nil)
   669  		require.NoError(t, err)
   670  		// set a malicious app path on the metadata
   671  		s.metadataRequest.Request.(*apiclient.AppStreamRequest_Metadata).Metadata.AppRelPath = "../out-of-bounds"
   672  		err = service.generateManifestGeneric(s)
   673  		require.ErrorContains(t, err, "illegal appPath")
   674  		assert.Nil(t, s.response)
   675  	})
   676  }
   677  
   678  type MockMatchRepositoryStream struct {
   679  	metadataSent    bool
   680  	fileSent        bool
   681  	metadataRequest *apiclient.AppStreamRequest
   682  	fileRequest     *apiclient.AppStreamRequest
   683  	response        *apiclient.RepositoryResponse
   684  }
   685  
   686  func NewMockMatchRepositoryStream(repoPath, appPath string, env []string) (*MockMatchRepositoryStream, error) {
   687  	tgz, mr, err := cmp.GetCompressedRepoAndMetadata(repoPath, appPath, env, nil, nil)
   688  	if err != nil {
   689  		return nil, err
   690  	}
   691  	defer tgzstream.CloseAndDelete(tgz)
   692  
   693  	tgzBuffer := bytes.NewBuffer(nil)
   694  	_, err = io.Copy(tgzBuffer, tgz)
   695  	if err != nil {
   696  		return nil, fmt.Errorf("failed to copy manifest targz to a byte buffer: %w", err)
   697  	}
   698  
   699  	return &MockMatchRepositoryStream{
   700  		metadataRequest: mr,
   701  		fileRequest:     cmp.AppFileRequest(tgzBuffer.Bytes()),
   702  	}, nil
   703  }
   704  
   705  func (m *MockMatchRepositoryStream) SendAndClose(response *apiclient.RepositoryResponse) error {
   706  	m.response = response
   707  	return nil
   708  }
   709  
   710  func (m *MockMatchRepositoryStream) Recv() (*apiclient.AppStreamRequest, error) {
   711  	if !m.metadataSent {
   712  		m.metadataSent = true
   713  		return m.metadataRequest, nil
   714  	}
   715  
   716  	if !m.fileSent {
   717  		m.fileSent = true
   718  		return m.fileRequest, nil
   719  	}
   720  	return nil, io.EOF
   721  }
   722  
   723  func (m *MockMatchRepositoryStream) Context() context.Context {
   724  	return context.Background()
   725  }
   726  
   727  func TestService_MatchRepository(t *testing.T) {
   728  	configFilePath := "./testdata/kustomize/config"
   729  	service, err := newService(configFilePath)
   730  	require.NoError(t, err)
   731  
   732  	t.Run("supported app", func(t *testing.T) {
   733  		s, err := NewMockMatchRepositoryStream("./testdata/kustomize", "./testdata/kustomize", nil)
   734  		require.NoError(t, err)
   735  		err = service.matchRepositoryGeneric(s)
   736  		require.NoError(t, err)
   737  		require.NotNil(t, s.response)
   738  		assert.True(t, s.response.IsSupported)
   739  	})
   740  
   741  	t.Run("unsupported app", func(t *testing.T) {
   742  		s, err := NewMockMatchRepositoryStream("./testdata/ksonnet", "./testdata/ksonnet", nil)
   743  		require.NoError(t, err)
   744  		err = service.matchRepositoryGeneric(s)
   745  		require.NoError(t, err)
   746  		require.NotNil(t, s.response)
   747  		assert.False(t, s.response.IsSupported)
   748  	})
   749  }
   750  
   751  type MockParametersAnnouncementStream struct {
   752  	metadataSent    bool
   753  	fileSent        bool
   754  	metadataRequest *apiclient.AppStreamRequest
   755  	fileRequest     *apiclient.AppStreamRequest
   756  	response        *apiclient.ParametersAnnouncementResponse
   757  }
   758  
   759  func NewMockParametersAnnouncementStream(repoPath, appPath string, env []string) (*MockParametersAnnouncementStream, error) {
   760  	tgz, mr, err := cmp.GetCompressedRepoAndMetadata(repoPath, appPath, env, nil, nil)
   761  	if err != nil {
   762  		return nil, err
   763  	}
   764  	defer tgzstream.CloseAndDelete(tgz)
   765  
   766  	tgzBuffer := bytes.NewBuffer(nil)
   767  	_, err = io.Copy(tgzBuffer, tgz)
   768  	if err != nil {
   769  		return nil, fmt.Errorf("failed to copy manifest targz to a byte buffer: %w", err)
   770  	}
   771  
   772  	return &MockParametersAnnouncementStream{
   773  		metadataRequest: mr,
   774  		fileRequest:     cmp.AppFileRequest(tgzBuffer.Bytes()),
   775  	}, nil
   776  }
   777  
   778  func (m *MockParametersAnnouncementStream) SendAndClose(response *apiclient.ParametersAnnouncementResponse) error {
   779  	m.response = response
   780  	return nil
   781  }
   782  
   783  func (m *MockParametersAnnouncementStream) Recv() (*apiclient.AppStreamRequest, error) {
   784  	if !m.metadataSent {
   785  		m.metadataSent = true
   786  		return m.metadataRequest, nil
   787  	}
   788  
   789  	if !m.fileSent {
   790  		m.fileSent = true
   791  		return m.fileRequest, nil
   792  	}
   793  	return nil, io.EOF
   794  }
   795  
   796  func (m *MockParametersAnnouncementStream) SetHeader(metadata.MD) error {
   797  	return nil
   798  }
   799  
   800  func (m *MockParametersAnnouncementStream) SendHeader(metadata.MD) error {
   801  	return nil
   802  }
   803  
   804  func (m *MockParametersAnnouncementStream) SetTrailer(metadata.MD) {}
   805  
   806  func (m *MockParametersAnnouncementStream) Context() context.Context {
   807  	return context.Background()
   808  }
   809  
   810  func (m *MockParametersAnnouncementStream) SendMsg(any) error {
   811  	return nil
   812  }
   813  
   814  func (m *MockParametersAnnouncementStream) RecvMsg(any) error {
   815  	return nil
   816  }
   817  
   818  func TestService_GetParametersAnnouncement(t *testing.T) {
   819  	configFilePath := "./testdata/kustomize/config"
   820  	service, err := newService(configFilePath)
   821  	require.NoError(t, err)
   822  
   823  	t.Run("successful response", func(t *testing.T) {
   824  		s, err := NewMockParametersAnnouncementStream("./testdata/kustomize", "./testdata/kustomize", []string{"MUST_BE_SET=yep"})
   825  		require.NoError(t, err)
   826  		err = service.GetParametersAnnouncement(s)
   827  		require.NoError(t, err)
   828  		require.NotNil(t, s.response)
   829  		require.Len(t, s.response.ParameterAnnouncements, 2)
   830  		assert.Equal(t, repoclient.ParameterAnnouncement{Name: "dynamic-test-param", String_: "yep"}, *s.response.ParameterAnnouncements[0])
   831  		assert.Equal(t, repoclient.ParameterAnnouncement{Name: "test-param", String_: "test-value"}, *s.response.ParameterAnnouncements[1])
   832  	})
   833  	t.Run("out of bounds app", func(t *testing.T) {
   834  		s, err := NewMockParametersAnnouncementStream("./testdata/kustomize", "./testdata/kustomize", []string{"MUST_BE_SET=yep"})
   835  		require.NoError(t, err)
   836  		// set a malicious app path on the metadata
   837  		s.metadataRequest.Request.(*apiclient.AppStreamRequest_Metadata).Metadata.AppRelPath = "../out-of-bounds"
   838  		err = service.GetParametersAnnouncement(s)
   839  		require.ErrorContains(t, err, "illegal appPath")
   840  		require.Nil(t, s.response)
   841  	})
   842  	t.Run("fails when script fails", func(t *testing.T) {
   843  		s, err := NewMockParametersAnnouncementStream("./testdata/kustomize", "./testdata/kustomize", []string{"WRONG_ENV_VAR=oops"})
   844  		require.NoError(t, err)
   845  		err = service.GetParametersAnnouncement(s)
   846  		require.ErrorContains(t, err, "error executing dynamic parameter output command")
   847  		require.Nil(t, s.response)
   848  	})
   849  }
   850  
   851  func TestService_CheckPluginConfiguration(t *testing.T) {
   852  	type fixture struct {
   853  		service *Service
   854  	}
   855  	setup := func(t *testing.T, opts ...pluginOpt) *fixture {
   856  		t.Helper()
   857  		cic := buildPluginConfig(opts...)
   858  		s := NewService(*cic)
   859  		return &fixture{
   860  			service: s,
   861  		}
   862  	}
   863  	t.Run("discovery is enabled when is configured", func(t *testing.T) {
   864  		// given
   865  		d := Discover{
   866  			FileName: "kustomization.yaml",
   867  		}
   868  		f := setup(t, withDiscover(d))
   869  
   870  		// when
   871  		resp, err := f.service.CheckPluginConfiguration(t.Context(), &empty.Empty{})
   872  
   873  		// then
   874  		require.NoError(t, err)
   875  		assert.True(t, resp.IsDiscoveryConfigured)
   876  	})
   877  
   878  	t.Run("discovery is disabled when is not configured", func(t *testing.T) {
   879  		// given
   880  		d := Discover{}
   881  		f := setup(t, withDiscover(d))
   882  
   883  		// when
   884  		resp, err := f.service.CheckPluginConfiguration(t.Context(), &empty.Empty{})
   885  
   886  		// then
   887  		require.NoError(t, err)
   888  		assert.False(t, resp.IsDiscoveryConfigured)
   889  	})
   890  }