Skip to content

Commit

Permalink
cmp parameter to enable git creds to be shared from repo server to th…
Browse files Browse the repository at this point in the history
…e plugin

Signed-off-by: jmcshane <[email protected]>
  • Loading branch information
jmcshane committed Oct 7, 2024
1 parent 20e2e78 commit d51b30d
Show file tree
Hide file tree
Showing 15 changed files with 249 additions and 46 deletions.
126 changes: 84 additions & 42 deletions cmpserver/apiclient/plugin.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cmpserver/plugin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type PluginConfigSpec struct {
Discover Discover `json:"discover"`
Parameters Parameters `yaml:"parameters"`
PreserveFileMode bool `json:"preserveFileMode,omitempty"`
ProvideGitCreds bool `json:"provideGitCreds,omitempty"`
}

// Discover holds find and fileName
Expand Down
2 changes: 1 addition & 1 deletion cmpserver/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ func getParametersAnnouncement(ctx context.Context, appDir string, announcements

func (s *Service) CheckPluginConfiguration(ctx context.Context, _ *empty.Empty) (*apiclient.CheckPluginConfigurationResponse, error) {
isDiscoveryConfigured := s.isDiscoveryConfigured()
response := &apiclient.CheckPluginConfigurationResponse{IsDiscoveryConfigured: isDiscoveryConfigured}
response := &apiclient.CheckPluginConfigurationResponse{IsDiscoveryConfigured: isDiscoveryConfigured, ProvideGitCreds: s.initConstants.PluginConfig.Spec.ProvideGitCreds}

return response, nil
}
Expand Down
1 change: 1 addition & 0 deletions cmpserver/plugin/plugin.proto
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ message File {
// CheckPluginConfigurationResponse contains a list of plugin configuration flags.
message CheckPluginConfigurationResponse {
bool isDiscoveryConfigured = 1;
bool provideGitCreds = 2;
}

// ConfigManagementPlugin Service
Expand Down
37 changes: 37 additions & 0 deletions docs/operator-manual/config-management-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ spec:
# If set to `true` then the plugin receives repository files with original file mode. Dangerous since the repository
# might have executable files. Set to true only if you trust the CMP plugin authors.
preserveFileMode: false

# If set to `true` then the plugin can retrieve git credentials from the reposerver during generate. Plugin authors
# should ensure these credentials are appropriately protected during execution
provideGitCreds: false
```
!!! note
Expand Down Expand Up @@ -503,3 +507,36 @@ spec:
args: ["sample args"]
preserveFileMode: true
```
##### Provide Git Credentials
By default, the config management plugin is responsible for providing its own credentials to additional Git repositories
that may need to be accessed during manifest generation. The reposerver has these credentials available in its git creds
store. When credential sharing is allowed, the git credentials used by the reposerver to clone the repository contents
are shared for the lifetime of the execution of the config management plugin, utilizing git's `ASKPASS` method to make a
call from the config management sidecar container to the reposerver to retrieve the initialized git credentials.

Utilizing `ASKPASS` means that credentials are not proactively shared, but rather only provided when an operation
requires them.

To allow the plugin to access the reposerver git credentials, you can set `provideGitCreds` to `true` in the plugin spec:

!!! warning
Make sure you trust the plugin you are using. If you set `provideGitCreds` to `true` then the plugin will receive
credentials used to clone the source Git repository.

```yaml
apiVersion: argoproj.io/v1alpha1
kind: ConfigManagementPlugin
metadata:
name: pluginName
spec:
init:
command: ["sample command"]
args: ["sample args"]
generate:
command: ["sample command"]
args: ["sample args"]
provideGitCreds: true
```

22 changes: 20 additions & 2 deletions reposerver/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
"golang.org/x/sync/semaphore"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"

"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand Down Expand Up @@ -1458,7 +1460,7 @@ func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string,
pluginName = q.ApplicationSource.Plugin.Name
}
// if pluginName is provided it has to be `<metadata.name>-<spec.version>` or just `<metadata.name>` if plugin version is empty
targetObjs, err = runConfigManagementPluginSidecars(ctx, appPath, repoRoot, pluginName, env, q, opt.cmpTarDoneCh, opt.cmpTarExcludedGlobs, opt.cmpUseManifestGeneratePaths)
targetObjs, err = runConfigManagementPluginSidecars(ctx, appPath, repoRoot, pluginName, env, q, q.Repo.GetGitCreds(gitCredsStore), opt.cmpTarDoneCh, opt.cmpTarExcludedGlobs, opt.cmpUseManifestGeneratePaths)
if err != nil {
err = fmt.Errorf("plugin sidecar failed. %s", err.Error())
}
Expand Down Expand Up @@ -1975,7 +1977,7 @@ func getPluginParamEnvs(envVars []string, plugin *v1alpha1.ApplicationSourcePlug
return env, nil
}

func runConfigManagementPluginSidecars(ctx context.Context, appPath, repoPath, pluginName string, envVars *v1alpha1.Env, q *apiclient.ManifestRequest, tarDoneCh chan<- bool, tarExcludedGlobs []string, useManifestGeneratePaths bool) ([]*unstructured.Unstructured, error) {
func runConfigManagementPluginSidecars(ctx context.Context, appPath, repoPath, pluginName string, envVars *v1alpha1.Env, q *apiclient.ManifestRequest, creds git.Creds, tarDoneCh chan<- bool, tarExcludedGlobs []string, useManifestGeneratePaths bool) ([]*unstructured.Unstructured, error) {
// compute variables.
env, err := getPluginEnvs(envVars, q)
if err != nil {
Expand All @@ -1996,6 +1998,22 @@ func runConfigManagementPluginSidecars(ctx context.Context, appPath, repoPath, p
log.Debugf("common root path calculated for application %s: %s", q.AppName, rootPath)
}

pluginConfigResponse, err := cmpClient.CheckPluginConfiguration(ctx, &emptypb.Empty{})
if err != nil {
return nil, fmt.Errorf("error calling cmp-server checkPluginConfiguration: %w", err)
}

if pluginConfigResponse.ProvideGitCreds {
if creds != nil {
closer, environ, err := creds.Environ()
if err != nil {
return nil, fmt.Errorf("failed to retrieve git creds environment variables: %w", err)
}
defer func() { _ = closer.Close() }()
env = append(env, environ...)
}
}

// generate manifests using commands provided in plugin config file in detected cmp-server sidecar
cmpManifests, err := generateManifestsCMP(ctx, appPath, rootPath, env, cmpClient, tarDoneCh, tarExcludedGlobs)
if err != nil {
Expand Down
72 changes: 71 additions & 1 deletion test/e2e/custom_tool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ func TestCustomToolWithGitCreds(t *testing.T) {
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy))
Expect(HealthIs(health.HealthStatusHealthy)).
And(func(app *Application) {
output, err := Run("", "kubectl", "-n", DeploymentNamespace(), "get", "cm", ctx.AppName(), "-o", "jsonpath={.metadata.annotations.GitAskpass}")
require.NoError(t, err)
assert.Equal(t, "argocd", output)
})
}

// make sure we can echo back the Git creds
Expand All @@ -65,6 +70,11 @@ func TestCustomToolWithGitCredsTemplate(t *testing.T) {
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
And(func(app *Application) {
output, err := Run("", "kubectl", "-n", DeploymentNamespace(), "get", "cm", ctx.AppName(), "-o", "jsonpath={.metadata.annotations.GitAskpass}")
require.NoError(t, err)
assert.Equal(t, "argocd", output)
}).
And(func(app *Application) {
output, err := Run("", "kubectl", "-n", DeploymentNamespace(), "get", "cm", ctx.AppName(), "-o", "jsonpath={.metadata.annotations.GitUsername}")
require.NoError(t, err)
Expand All @@ -77,6 +87,66 @@ func TestCustomToolWithGitCredsTemplate(t *testing.T) {
})
}

// make sure we can read the Git creds stored in a temporary file
func TestCustomToolWithSSHGitCreds(t *testing.T) {
ctx := Given(t)
// path does not matter, we ignore it
ctx.
And(func() {
go startCMPServer(t, "./testdata/cmp-gitsshcreds")
time.Sleep(1 * time.Second)
t.Setenv("ARGOCD_BINARY_NAME", "argocd")
}).
CustomCACertAdded().
// add the private repo with ssh credentials
CustomSSHKnownHostsAdded().
SSHRepoURLAdded(true).
RepoURLType(RepoURLTypeSSH).
Path("cmp-gitsshcreds").
When().
CreateApp().
Sync().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
And(func(app *Application) {
output, err := Run("", "kubectl", "-n", DeploymentNamespace(), "get", "cm", Name(), "-o", "jsonpath={.metadata.annotations.GitSSHCommand}")
require.NoError(t, err)
assert.Regexp(t, `-i [^ ]+`, output, "test plugin expects $GIT_SSH_COMMAND to contain the option '-i <path to ssh private key>'")
}).
And(func(app *Application) {
output, err := Run("", "kubectl", "-n", DeploymentNamespace(), "get", "cm", Name(), "-o", "jsonpath={.metadata.annotations.GitSSHCredsFileSHA}")
require.NoError(t, err)
assert.Regexp(t, `\w+\s+[\/\w]+`, output, "git ssh credentials file should be able to be read, hashing the contents")
})
}

func TestCustomToolWithSSHGitCredsDisabled(t *testing.T) {
ctx := Given(t)
// path does not matter, we ignore it
ctx.
And(func() {
go startCMPServer(t, "./testdata/cmp-gitsshcreds-disable-provide")
time.Sleep(1 * time.Second)
t.Setenv("ARGOCD_BINARY_NAME", "argocd")
}).
CustomCACertAdded().
// add the private repo with ssh credentials
CustomSSHKnownHostsAdded().
SSHRepoURLAdded(true).
RepoURLType(RepoURLTypeSSH).
Path("cmp-gitsshcreds").
When().
CreateApp().
Sync().
Sync().
Then().
Expect(OperationPhaseIs(OperationFailed)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync))
}

// make sure we can echo back the env
func TestCustomToolWithEnv(t *testing.T) {
ctx := Given(t)
Expand Down
Loading

0 comments on commit d51b30d

Please sign in to comment.