Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
803 changes: 45 additions & 758 deletions cmpserver/apiclient/plugin.pb.go

Large diffs are not rendered by default.

18 changes: 3 additions & 15 deletions cmpserver/plugin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
configUtil "github.com/argoproj/argo-cd/v2/util/config"
)

Expand Down Expand Up @@ -50,21 +51,8 @@ type Find struct {

// Parameters holds static and dynamic configurations
type Parameters struct {
Static []Static `yaml:"static"`
Dynamic Command `yaml:"dynamic"`
}

// Static hold the static announcements for CMP's
type Static struct {
Name string `yaml:"name,omitempty"`
Title string `yaml:"title,omitempty"`
Tooltip string `yaml:"tooltip,omitempty"`
Required bool `yaml:"required,omitempty"`
ItemType string `yaml:"itemType,omitempty"`
CollectionType string `yaml:"collectionType,omitempty"`
String string `yaml:"string,omitempty"`
Array []string `yaml:"array,omitempty"`
Map map[string]string `yaml:"map,omitempty"`
Static []*apiclient.ParameterAnnouncement `yaml:"static"`
Dynamic Command `yaml:"dynamic"`
}

// Dynamic hold the dynamic announcements for CMP's
Expand Down
69 changes: 28 additions & 41 deletions cmpserver/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/argoproj/argo-cd/v2/cmpserver/apiclient"
"github.com/argoproj/argo-cd/v2/common"
repoclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient"
"github.com/argoproj/argo-cd/v2/util/buffered_context"
"github.com/argoproj/argo-cd/v2/util/cmp"
"github.com/argoproj/argo-cd/v2/util/io/files"
Expand Down Expand Up @@ -143,20 +144,31 @@ func environ(envVars []*apiclient.EnvEntry) []string {
return environ
}

// getTempDirMustCleanup creates a temporary directory and returns a cleanup function. The cleanup function panics if
// cleanup fails. Use this function when failing to clean up the temporary directory is a security risk.
func getTempDirMustCleanup(baseDir string) (workDir string, cleanup func(), err error) {
workDir, err = files.CreateTempDir(baseDir)
if err != nil {
return "", nil, fmt.Errorf("error creating temp dir: %s", err)
}
cleanup = func() {
if err := os.RemoveAll(workDir); err != nil {
// we panic here as the workDir may contain sensitive information
panic(fmt.Sprintf("error removing plugin workdir: %s", err))
}
}
return workDir, cleanup, nil
}

// GenerateManifest runs generate command from plugin config file and returns generated manifest files
func (s *Service) GenerateManifest(stream apiclient.ConfigManagementPluginService_GenerateManifestServer) error {
ctx, cancel := buffered_context.WithEarlierDeadline(stream.Context(), cmpTimeoutBuffer)
defer cancel()
workDir, err := files.CreateTempDir(common.GetCMPWorkDir())
workDir, cleanup, err := getTempDirMustCleanup(common.GetCMPWorkDir())
if err != nil {
return fmt.Errorf("error creating temp dir: %s", err)
return fmt.Errorf("error creating workdir for manifest generation: %s", err)
}
defer func() {
if err := os.RemoveAll(workDir); err != nil {
// we panic here as the workDir may contain sensitive information
panic(fmt.Sprintf("error removing generate manifest workdir: %s", err))
}
}()
defer cleanup()

metadata, err := cmp.ReceiveRepoStream(ctx, stream, workDir)
if err != nil {
Expand Down Expand Up @@ -222,16 +234,11 @@ func (s *Service) MatchRepository(stream apiclient.ConfigManagementPluginService
bufferedCtx, cancel := buffered_context.WithEarlierDeadline(stream.Context(), cmpTimeoutBuffer)
defer cancel()

workDir, err := files.CreateTempDir(common.GetCMPWorkDir())
workDir, cleanup, err := getTempDirMustCleanup(common.GetCMPWorkDir())
if err != nil {
return fmt.Errorf("error creating match repository workdir: %s", err)
return fmt.Errorf("error creating workdir for repository matching: %s", err)
}
defer func() {
if err := os.RemoveAll(workDir); err != nil {
// we panic here as the workDir may contain sensitive information
panic(fmt.Sprintf("error removing match repository workdir: %s", err))
}
}()
defer cleanup()

_, err = cmp.ReceiveRepoStream(bufferedCtx, stream, workDir)
if err != nil {
Expand Down Expand Up @@ -299,16 +306,11 @@ func (s *Service) GetParametersAnnouncement(stream apiclient.ConfigManagementPlu
bufferedCtx, cancel := buffered_context.WithEarlierDeadline(stream.Context(), cmpTimeoutBuffer)
defer cancel()

workDir, err := files.CreateTempDir(common.GetCMPWorkDir())
workDir, cleanup, err := getTempDirMustCleanup(common.GetCMPWorkDir())
if err != nil {
return fmt.Errorf("error creating parameters announcement workdir: %s", err)
return fmt.Errorf("error creating workdir for generating parameter announcements: %s", err)
}
defer func() {
if err := os.RemoveAll(workDir); err != nil {
// we panic here as the workDir may contain sensitive information
panic(fmt.Sprintf("error removing parameters announcement repository workdir: %s", err))
}
}()
defer cleanup()

metadata, err := cmp.ReceiveRepoStream(bufferedCtx, stream, workDir)
if err != nil {
Expand All @@ -331,29 +333,14 @@ func (s *Service) GetParametersAnnouncement(stream apiclient.ConfigManagementPlu
return nil
}

func getParametersAnnouncement(ctx context.Context, appDir string, staticAnnouncements []Static, command Command) (*apiclient.ParametersAnnouncementResponse, error) {
var announcements []*apiclient.ParameterAnnouncement
for _, static := range staticAnnouncements {
announcements = append(announcements, &apiclient.ParameterAnnouncement{
Name: static.Name,
Title: static.Title,
Tooltip: static.Tooltip,
Required: static.Required,
ItemType: static.ItemType,
CollectionType: static.CollectionType,
String_: static.String,
Array: static.Array,
Map: static.Map,
})
}

func getParametersAnnouncement(ctx context.Context, appDir string, announcements []*repoclient.ParameterAnnouncement, command Command) (*apiclient.ParametersAnnouncementResponse, error) {
if len(command.Command) > 0 {
stdout, err := runCommand(ctx, command, appDir, os.Environ())
if err != nil {
return nil, fmt.Errorf("error executing dynamic parameter output command: %s", err)
}

var dynamicParamAnnouncements []*apiclient.ParameterAnnouncement
var dynamicParamAnnouncements []*repoclient.ParameterAnnouncement
err = json.Unmarshal([]byte(stdout), &dynamicParamAnnouncements)
if err != nil {
return nil, fmt.Errorf("error unmarshaling dynamic parameter output into ParametersAnnouncementResponse: %s", err)
Expand Down
28 changes: 3 additions & 25 deletions cmpserver/plugin/plugin.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ option go_package = "github.com/argoproj/argo-cd/v2/cmpserver/apiclient";

package plugin;

import "github.com/argoproj/argo-cd/v2/reposerver/repository/repository.proto";

// AppStreamRequest is the request object used to send the application's
// files over a stream.
message AppStreamRequest {
Expand Down Expand Up @@ -44,34 +46,10 @@ message RepositoryResponse {
bool isSupported = 1;
}

message ParameterAnnouncement {
// name is the name identifying a parameter.
string name = 1;
// title is a human-readable text of the parameter name.
string title = 2;
// tooltip is a human-readable description of the parameter.
string tooltip = 3;
// required defines if this given parameter is mandatory.
bool required = 4;
// itemType determines the primitive data type represented by the parameter. Parameters are always encoded as
// strings, but this field lets them be interpreted as other primitive types.
string itemType = 5;
// collectionType is the type of value this parameter holds - either a single value (a string) or a collection
// (array or map). If collectionType is set, only the field with that type will be used. If collectionType is not
// set, `string` is the default. If collectionType is set to an invalid value, a validation error is thrown.
string collectionType = 6;
// string is the default value of the parameter if the parameter is a string.
string string = 7;
// array is the default value of the parameter if the parameter is an array.
repeated string array = 8;
// map is the default value of the parameter if the parameter is a map.
map<string, string> map = 9;
}

// ParametersAnnouncementResponse contains a list of announcements. This list represents all the parameters which a CMP
// is able to accept.
message ParametersAnnouncementResponse {
repeated ParameterAnnouncement parameterAnnouncements = 1;
repeated repository.ParameterAnnouncement parameterAnnouncements = 1;
}

message File {
Expand Down
38 changes: 29 additions & 9 deletions cmpserver/plugin/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package plugin

import (
"context"
"os"
"path/filepath"
"testing"
"time"
Expand All @@ -11,7 +12,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"
)

Expand Down Expand Up @@ -234,7 +235,7 @@ func Test_getParametersAnnouncement_empty_command(t *testing.T) {
- name: static-a
- name: static-b
`
static := &[]Static{}
static := &[]*repoclient.ParameterAnnouncement{}
err := yaml.Unmarshal([]byte(staticYAML), static)
require.NoError(t, err)
command := Command{
Expand All @@ -243,29 +244,29 @@ func Test_getParametersAnnouncement_empty_command(t *testing.T) {
}
res, err := getParametersAnnouncement(context.Background(), "", *static, command)
require.NoError(t, err)
assert.Equal(t, []*apiclient.ParameterAnnouncement{{Name: "static-a"}, {Name: "static-b"}}, res.ParameterAnnouncements)
assert.Equal(t, []*repoclient.ParameterAnnouncement{{Name: "static-a"}, {Name: "static-b"}}, res.ParameterAnnouncements)
}

func Test_getParametersAnnouncement_no_command(t *testing.T) {
staticYAML := `
- name: static-a
- name: static-b
`
static := &[]Static{}
static := &[]*repoclient.ParameterAnnouncement{}
err := yaml.Unmarshal([]byte(staticYAML), static)
require.NoError(t, err)
command := Command{}
res, err := getParametersAnnouncement(context.Background(), "", *static, command)
require.NoError(t, err)
assert.Equal(t, []*apiclient.ParameterAnnouncement{{Name: "static-a"}, {Name: "static-b"}}, res.ParameterAnnouncements)
assert.Equal(t, []*repoclient.ParameterAnnouncement{{Name: "static-a"}, {Name: "static-b"}}, res.ParameterAnnouncements)
}

func Test_getParametersAnnouncement_static_and_dynamic(t *testing.T) {
staticYAML := `
- name: static-a
- name: static-b
`
static := &[]Static{}
static := &[]*repoclient.ParameterAnnouncement{}
err := yaml.Unmarshal([]byte(staticYAML), static)
require.NoError(t, err)
command := Command{
Expand All @@ -274,7 +275,7 @@ func Test_getParametersAnnouncement_static_and_dynamic(t *testing.T) {
}
res, err := getParametersAnnouncement(context.Background(), "", *static, command)
require.NoError(t, err)
expected := []*apiclient.ParameterAnnouncement{
expected := []*repoclient.ParameterAnnouncement{
{Name: "dynamic-a"},
{Name: "dynamic-b"},
{Name: "static-a"},
Expand All @@ -288,7 +289,7 @@ func Test_getParametersAnnouncement_invalid_json(t *testing.T) {
Command: []string{"echo"},
Args: []string{`[`},
}
_, err := getParametersAnnouncement(context.Background(), "", []Static{}, command)
_, err := getParametersAnnouncement(context.Background(), "", []*repoclient.ParameterAnnouncement{}, command)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unexpected end of JSON input")
}
Expand All @@ -298,7 +299,26 @@ func Test_getParametersAnnouncement_bad_command(t *testing.T) {
Command: []string{"exit"},
Args: []string{"1"},
}
_, err := getParametersAnnouncement(context.Background(), "", []Static{}, command)
_, err := getParametersAnnouncement(context.Background(), "", []*repoclient.ParameterAnnouncement{}, command)
assert.Error(t, err)
assert.Contains(t, err.Error(), "error executing dynamic parameter output command")
}

func Test_getTempDirMustCleanup(t *testing.T) {
tempDir := t.TempDir()
workDir, cleanup, err := getTempDirMustCleanup(tempDir)
require.NoError(t, err)
require.DirExists(t, workDir)

// Induce a cleanup error to verify panic behavior.
err = os.Chmod(tempDir, 0000)
require.NoError(t, err)
assert.Panics(t, func() {
cleanup()
}, "cleanup must panic to protect from directory traversal vulnerabilities")

err = os.Chmod(tempDir, 0700)
require.NoError(t, err)
cleanup()
assert.NoDirExists(t, workDir)
}
10 changes: 10 additions & 0 deletions docs/operator-manual/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ spec:

# plugin specific config
plugin:
# NOTE: this field is deprecated in v2.4 and must be removed to use sidecar-based plugins.
# Only set the plugin name if the plugin is defined in argocd-cm.
# If the plugin is defined as a sidecar, omit the name. The plugin will be automatically matched with the
# Application according to the plugin's discovery rules.
Expand All @@ -103,6 +104,15 @@ spec:
env:
- name: FOO
value: bar
# Plugin parameters are new in v2.4.
parameters:
- name: string-param
string: example-string
- name: array-param
array: [item1, item2]
- name: map-param
map:
param-name: param-value

# Destination cluster and namespace to deploy the application
destination:
Expand Down
Loading