mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
Merge 9165e775f9 into a7853eb7b6
This commit is contained in:
commit
bdbcd2a4a9
94 changed files with 6814 additions and 2769 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Prevent vendor directory from being copied to ensure we are not not pulling unexpected cruft from
|
||||
# Prevent vendor directory from being copied to ensure we are not not pulling unexpected cruft from
|
||||
# a user's workspace, and are only building off of what is locked by dep.
|
||||
.vscode/
|
||||
.idea/
|
||||
|
|
@ -26,4 +26,4 @@ examples/
|
|||
!hack/gpg-wrapper.sh
|
||||
!hack/git-verify-wrapper.sh
|
||||
!hack/tool-versions.sh
|
||||
!hack/install.sh
|
||||
!hack/install.sh
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ RUN touch ssh_known_hosts && \
|
|||
WORKDIR /app/config
|
||||
RUN mkdir -p tls && \
|
||||
mkdir -p gpg/source && \
|
||||
mkdir -p gpg/keys
|
||||
mkdir -p gpg/keys
|
||||
|
||||
COPY .tilt-bin/argocd_linux /usr/local/bin/argocd
|
||||
|
||||
|
|
@ -59,4 +59,4 @@ RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-server && \
|
|||
RUN mkdir -p /tilt
|
||||
|
||||
# overridden by Tiltfile
|
||||
ENTRYPOINT ["/usr/bin/tini", "-s", "--", "dlv", "exec", "--continue", "--accept-multiclient", "--headless", "--listen=:2345", "--api-version=2"]
|
||||
ENTRYPOINT ["/usr/bin/tini", "-s", "--", "dlv", "exec", "--continue", "--accept-multiclient", "--headless", "--listen=:2345", "--api-version=2"]
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/applicationset/services"
|
||||
"github.com/argoproj/argo-cd/v3/applicationset/utils"
|
||||
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg"
|
||||
)
|
||||
|
||||
var _ Generator = (*GitGenerator)(nil)
|
||||
|
|
@ -70,7 +69,7 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
|
|||
|
||||
noRevisionCache := appSet.RefreshRequired()
|
||||
|
||||
verifyCommit := false
|
||||
var sourceIntegrity *argoprojiov1alpha1.SourceIntegrity
|
||||
|
||||
// When the project field is templated, the contents of the git repo are required to run the git generator and get the templated value,
|
||||
// but git generator cannot be called without verifying the commit signature.
|
||||
|
|
@ -83,11 +82,13 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
|
|||
if controllerNamespace == "" {
|
||||
controllerNamespace = appSet.Namespace
|
||||
}
|
||||
|
||||
if err := client.Get(context.TODO(), types.NamespacedName{Name: project, Namespace: controllerNamespace}, appProject); err != nil {
|
||||
return nil, fmt.Errorf("error getting project %s: %w", project, err)
|
||||
}
|
||||
// we need to verify the signature on the Git revision if GPG is enabled
|
||||
verifyCommit = len(appProject.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled()
|
||||
sourceIntegrity = appProject.EffectiveSourceIntegrity()
|
||||
} else {
|
||||
log.WithField("appset", appSet.Name).Infof("Cannot enforce eventual source integrity, app project name is templated")
|
||||
}
|
||||
|
||||
// If the project field is templated, we cannot resolve the project name, so we pass an empty string to the repo-server.
|
||||
|
|
@ -98,9 +99,9 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
|
|||
var res []map[string]any
|
||||
switch {
|
||||
case len(appSetGenerator.Git.Directories) != 0:
|
||||
res, err = g.generateParamsForGitDirectories(appSetGenerator, noRevisionCache, verifyCommit, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions)
|
||||
res, err = g.generateParamsForGitDirectories(appSetGenerator, noRevisionCache, sourceIntegrity, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions)
|
||||
case len(appSetGenerator.Git.Files) != 0:
|
||||
res, err = g.generateParamsForGitFiles(appSetGenerator, noRevisionCache, verifyCommit, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions)
|
||||
res, err = g.generateParamsForGitFiles(appSetGenerator, noRevisionCache, sourceIntegrity, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions)
|
||||
default:
|
||||
return nil, ErrEmptyAppSetGenerator
|
||||
}
|
||||
|
|
@ -114,8 +115,8 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
|
|||
// generateParamsForGitDirectories generates parameters for an ApplicationSet using a directory-based Git generator.
|
||||
// It fetches all directories from the given Git repository and revision, optionally using a revision cache and verifying commits.
|
||||
// It then filters the directories based on the generator's configuration and renders parameters for the resulting applications
|
||||
func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache, verifyCommit, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) {
|
||||
allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, project, noRevisionCache, verifyCommit)
|
||||
func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache bool, sourceIntegrity *argoprojiov1alpha1.SourceIntegrity, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) {
|
||||
allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, project, noRevisionCache, sourceIntegrity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting directories from repo: %w", err)
|
||||
}
|
||||
|
|
@ -141,7 +142,7 @@ func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoproj
|
|||
// generateParamsForGitFiles generates parameters for an ApplicationSet using a file-based Git generator.
|
||||
// It retrieves and processes specified files from the Git repository, supporting both YAML and JSON formats,
|
||||
// and returns a list of parameter maps extracted from the content.
|
||||
func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache, verifyCommit, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) {
|
||||
func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache bool, sourceIntegrity *argoprojiov1alpha1.SourceIntegrity, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) {
|
||||
// fileContentMap maps absolute file paths to their byte content
|
||||
fileContentMap := make(map[string][]byte)
|
||||
var includePatterns []string
|
||||
|
|
@ -164,7 +165,7 @@ func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1al
|
|||
project,
|
||||
includePattern,
|
||||
noRevisionCache,
|
||||
verifyCommit,
|
||||
sourceIntegrity,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -181,7 +182,7 @@ func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1al
|
|||
project,
|
||||
excludePattern,
|
||||
noRevisionCache,
|
||||
verifyCommit,
|
||||
sourceIntegrity,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
61
applicationset/services/mocks/Repos.go
generated
61
applicationset/services/mocks/Repos.go
generated
|
|
@ -7,6 +7,7 @@ package mocks
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
|
|
@ -38,8 +39,8 @@ func (_m *Repos) EXPECT() *Repos_Expecter {
|
|||
}
|
||||
|
||||
// GetDirectories provides a mock function for the type Repos
|
||||
func (_mock *Repos) GetDirectories(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, verifyCommit bool) ([]string, error) {
|
||||
ret := _mock.Called(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
|
||||
func (_mock *Repos) GetDirectories(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) ([]string, error) {
|
||||
ret := _mock.Called(ctx, repoURL, revision, project, noRevisionCache, sourceIntegrity)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetDirectories")
|
||||
|
|
@ -47,18 +48,18 @@ func (_mock *Repos) GetDirectories(ctx context.Context, repoURL string, revision
|
|||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, bool, bool) ([]string, error)); ok {
|
||||
return returnFunc(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, bool, *v1alpha1.SourceIntegrity) ([]string, error)); ok {
|
||||
return returnFunc(ctx, repoURL, revision, project, noRevisionCache, sourceIntegrity)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, bool, bool) []string); ok {
|
||||
r0 = returnFunc(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, bool, *v1alpha1.SourceIntegrity) []string); ok {
|
||||
r0 = returnFunc(ctx, repoURL, revision, project, noRevisionCache, sourceIntegrity)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, bool, bool) error); ok {
|
||||
r1 = returnFunc(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, bool, *v1alpha1.SourceIntegrity) error); ok {
|
||||
r1 = returnFunc(ctx, repoURL, revision, project, noRevisionCache, sourceIntegrity)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
|
@ -76,12 +77,12 @@ type Repos_GetDirectories_Call struct {
|
|||
// - revision string
|
||||
// - project string
|
||||
// - noRevisionCache bool
|
||||
// - verifyCommit bool
|
||||
func (_e *Repos_Expecter) GetDirectories(ctx interface{}, repoURL interface{}, revision interface{}, project interface{}, noRevisionCache interface{}, verifyCommit interface{}) *Repos_GetDirectories_Call {
|
||||
return &Repos_GetDirectories_Call{Call: _e.mock.On("GetDirectories", ctx, repoURL, revision, project, noRevisionCache, verifyCommit)}
|
||||
// - sourceIntegrity *v1alpha1.SourceIntegrity
|
||||
func (_e *Repos_Expecter) GetDirectories(ctx interface{}, repoURL interface{}, revision interface{}, project interface{}, noRevisionCache interface{}, sourceIntegrity interface{}) *Repos_GetDirectories_Call {
|
||||
return &Repos_GetDirectories_Call{Call: _e.mock.On("GetDirectories", ctx, repoURL, revision, project, noRevisionCache, sourceIntegrity)}
|
||||
}
|
||||
|
||||
func (_c *Repos_GetDirectories_Call) Run(run func(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, verifyCommit bool)) *Repos_GetDirectories_Call {
|
||||
func (_c *Repos_GetDirectories_Call) Run(run func(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity)) *Repos_GetDirectories_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
|
|
@ -103,9 +104,9 @@ func (_c *Repos_GetDirectories_Call) Run(run func(ctx context.Context, repoURL s
|
|||
if args[4] != nil {
|
||||
arg4 = args[4].(bool)
|
||||
}
|
||||
var arg5 bool
|
||||
var arg5 *v1alpha1.SourceIntegrity
|
||||
if args[5] != nil {
|
||||
arg5 = args[5].(bool)
|
||||
arg5 = args[5].(*v1alpha1.SourceIntegrity)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
|
|
@ -124,14 +125,14 @@ func (_c *Repos_GetDirectories_Call) Return(strings []string, err error) *Repos_
|
|||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repos_GetDirectories_Call) RunAndReturn(run func(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, verifyCommit bool) ([]string, error)) *Repos_GetDirectories_Call {
|
||||
func (_c *Repos_GetDirectories_Call) RunAndReturn(run func(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) ([]string, error)) *Repos_GetDirectories_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetFiles provides a mock function for the type Repos
|
||||
func (_mock *Repos) GetFiles(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, verifyCommit bool) (map[string][]byte, error) {
|
||||
ret := _mock.Called(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
|
||||
func (_mock *Repos) GetFiles(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) (map[string][]byte, error) {
|
||||
ret := _mock.Called(ctx, repoURL, revision, project, pattern, noRevisionCache, sourceIntegrity)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetFiles")
|
||||
|
|
@ -139,18 +140,18 @@ func (_mock *Repos) GetFiles(ctx context.Context, repoURL string, revision strin
|
|||
|
||||
var r0 map[string][]byte
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, bool) (map[string][]byte, error)); ok {
|
||||
return returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, *v1alpha1.SourceIntegrity) (map[string][]byte, error)); ok {
|
||||
return returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, sourceIntegrity)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, bool) map[string][]byte); ok {
|
||||
r0 = returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, *v1alpha1.SourceIntegrity) map[string][]byte); ok {
|
||||
r0 = returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, sourceIntegrity)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string][]byte)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, string, bool, bool) error); ok {
|
||||
r1 = returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, string, bool, *v1alpha1.SourceIntegrity) error); ok {
|
||||
r1 = returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, sourceIntegrity)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
|
@ -169,12 +170,12 @@ type Repos_GetFiles_Call struct {
|
|||
// - project string
|
||||
// - pattern string
|
||||
// - noRevisionCache bool
|
||||
// - verifyCommit bool
|
||||
func (_e *Repos_Expecter) GetFiles(ctx interface{}, repoURL interface{}, revision interface{}, project interface{}, pattern interface{}, noRevisionCache interface{}, verifyCommit interface{}) *Repos_GetFiles_Call {
|
||||
return &Repos_GetFiles_Call{Call: _e.mock.On("GetFiles", ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)}
|
||||
// - sourceIntegrity *v1alpha1.SourceIntegrity
|
||||
func (_e *Repos_Expecter) GetFiles(ctx interface{}, repoURL interface{}, revision interface{}, project interface{}, pattern interface{}, noRevisionCache interface{}, sourceIntegrity interface{}) *Repos_GetFiles_Call {
|
||||
return &Repos_GetFiles_Call{Call: _e.mock.On("GetFiles", ctx, repoURL, revision, project, pattern, noRevisionCache, sourceIntegrity)}
|
||||
}
|
||||
|
||||
func (_c *Repos_GetFiles_Call) Run(run func(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, verifyCommit bool)) *Repos_GetFiles_Call {
|
||||
func (_c *Repos_GetFiles_Call) Run(run func(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity)) *Repos_GetFiles_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
|
|
@ -200,9 +201,9 @@ func (_c *Repos_GetFiles_Call) Run(run func(ctx context.Context, repoURL string,
|
|||
if args[5] != nil {
|
||||
arg5 = args[5].(bool)
|
||||
}
|
||||
var arg6 bool
|
||||
var arg6 *v1alpha1.SourceIntegrity
|
||||
if args[6] != nil {
|
||||
arg6 = args[6].(bool)
|
||||
arg6 = args[6].(*v1alpha1.SourceIntegrity)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
|
|
@ -222,7 +223,7 @@ func (_c *Repos_GetFiles_Call) Return(stringToBytes map[string][]byte, err error
|
|||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repos_GetFiles_Call) RunAndReturn(run func(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, verifyCommit bool) (map[string][]byte, error)) *Repos_GetFiles_Call {
|
||||
func (_c *Repos_GetFiles_Call) RunAndReturn(run func(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) (map[string][]byte, error)) *Repos_GetFiles_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ type argoCDService struct {
|
|||
|
||||
type Repos interface {
|
||||
// GetFiles returns content of files (not directories) within the target repo
|
||||
GetFiles(ctx context.Context, repoURL, revision, project, pattern string, noRevisionCache, verifyCommit bool) (map[string][]byte, error)
|
||||
GetFiles(ctx context.Context, repoURL, revision, project, pattern string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) (map[string][]byte, error)
|
||||
|
||||
// GetDirectories returns a list of directories (not files) within the target repo
|
||||
GetDirectories(ctx context.Context, repoURL, revision, project string, noRevisionCache, verifyCommit bool) ([]string, error)
|
||||
GetDirectories(ctx context.Context, repoURL, revision, project string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) ([]string, error)
|
||||
}
|
||||
|
||||
func NewArgoCDService(db db.ArgoDB, submoduleEnabled bool, repoClientset apiclient.Clientset, newFileGlobbingEnabled bool) Repos {
|
||||
|
|
@ -50,7 +50,7 @@ func NewArgoCDService(db db.ArgoDB, submoduleEnabled bool, repoClientset apiclie
|
|||
}
|
||||
}
|
||||
|
||||
func (a *argoCDService) GetFiles(ctx context.Context, repoURL, revision, project, pattern string, noRevisionCache, verifyCommit bool) (map[string][]byte, error) {
|
||||
func (a *argoCDService) GetFiles(ctx context.Context, repoURL, revision, project, pattern string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) (map[string][]byte, error) {
|
||||
repo, err := a.getRepository(ctx, repoURL, project)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in GetRepository: %w", err)
|
||||
|
|
@ -63,8 +63,9 @@ func (a *argoCDService) GetFiles(ctx context.Context, repoURL, revision, project
|
|||
Path: pattern,
|
||||
NewGitFileGlobbingEnabled: a.newFileGlobbingEnabled,
|
||||
NoRevisionCache: noRevisionCache,
|
||||
VerifyCommit: verifyCommit,
|
||||
SourceIntegrity: sourceIntegrity,
|
||||
}
|
||||
|
||||
fileResponse, err := a.getGitFilesFromRepoServer(ctx, fileRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving Git files: %w", err)
|
||||
|
|
@ -72,7 +73,7 @@ func (a *argoCDService) GetFiles(ctx context.Context, repoURL, revision, project
|
|||
return fileResponse.GetMap(), nil
|
||||
}
|
||||
|
||||
func (a *argoCDService) GetDirectories(ctx context.Context, repoURL, revision, project string, noRevisionCache, verifyCommit bool) ([]string, error) {
|
||||
func (a *argoCDService) GetDirectories(ctx context.Context, repoURL, revision, project string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) ([]string, error) {
|
||||
repo, err := a.getRepository(ctx, repoURL, project)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in GetRepository: %w", err)
|
||||
|
|
@ -83,7 +84,7 @@ func (a *argoCDService) GetDirectories(ctx context.Context, repoURL, revision, p
|
|||
SubmoduleEnabled: a.submoduleEnabled,
|
||||
Revision: revision,
|
||||
NoRevisionCache: noRevisionCache,
|
||||
VerifyCommit: verifyCommit,
|
||||
SourceIntegrity: sourceIntegrity,
|
||||
}
|
||||
|
||||
dirResponse, err := a.getGitDirectoriesFromRepoServer(ctx, dirRequest)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ func TestGetDirectories(t *testing.T) {
|
|||
repoURL string
|
||||
revision string
|
||||
noRevisionCache bool
|
||||
verifyCommit bool
|
||||
sourceIntegrity *v1alpha1.SourceIntegrity
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -80,7 +80,7 @@ func TestGetDirectories(t *testing.T) {
|
|||
submoduleEnabled: tt.fields.submoduleEnabled,
|
||||
getGitDirectoriesFromRepoServer: tt.fields.getGitDirectories,
|
||||
}
|
||||
got, err := a.GetDirectories(tt.args.ctx, tt.args.repoURL, tt.args.revision, "", tt.args.noRevisionCache, tt.args.verifyCommit)
|
||||
got, err := a.GetDirectories(tt.args.ctx, tt.args.repoURL, tt.args.revision, "", tt.args.noRevisionCache, tt.args.sourceIntegrity)
|
||||
if !tt.wantErr(t, err, fmt.Sprintf("GetDirectories(%v, %v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.noRevisionCache)) {
|
||||
return
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ func TestGetFiles(t *testing.T) {
|
|||
revision string
|
||||
pattern string
|
||||
noRevisionCache bool
|
||||
verifyCommit bool
|
||||
sourceIntegrity *v1alpha1.SourceIntegrity
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -159,7 +159,7 @@ func TestGetFiles(t *testing.T) {
|
|||
submoduleEnabled: tt.fields.submoduleEnabled,
|
||||
getGitFilesFromRepoServer: tt.fields.getGitFiles,
|
||||
}
|
||||
got, err := a.GetFiles(tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern, "", tt.args.noRevisionCache, tt.args.verifyCommit)
|
||||
got, err := a.GetFiles(tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern, "", tt.args.noRevisionCache, tt.args.sourceIntegrity)
|
||||
if !tt.wantErr(t, err, fmt.Sprintf("GetFiles(%v, %v, %v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern, tt.args.noRevisionCache)) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
103
assets/swagger.json
generated
103
assets/swagger.json
generated
|
|
@ -6152,12 +6152,15 @@
|
|||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceIntegrityResult": {
|
||||
"$ref": "#/definitions/v1alpha1SourceIntegrityCheckResult"
|
||||
},
|
||||
"sourceType": {
|
||||
"type": "string"
|
||||
},
|
||||
"verifyResult": {
|
||||
"type": "string",
|
||||
"title": "Raw response of git verify-commit operation (always the empty string for Helm)"
|
||||
"description": "Raw response of git verify-commit operation (always the empty string for Helm)\nDeprecated: Use SourceIntegrityResult for more detailed information. VerifyResult will be removed with the next major version.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -7041,6 +7044,9 @@
|
|||
"$ref": "#/definitions/v1alpha1SignatureKey"
|
||||
}
|
||||
},
|
||||
"sourceIntegrity": {
|
||||
"$ref": "#/definitions/v1alpha1SourceIntegrity"
|
||||
},
|
||||
"sourceNamespaces": {
|
||||
"type": "array",
|
||||
"title": "SourceNamespaces defines the namespaces application resources are allowed to be created in",
|
||||
|
|
@ -10278,9 +10284,12 @@
|
|||
}
|
||||
},
|
||||
"signatureInfo": {
|
||||
"description": "SignatureInfo contains a hint on the signer if the revision was signed with GPG, and signature verification is enabled.",
|
||||
"description": "SignatureInfo contains a hint on the signer if the revision was signed with GPG, and signature verification is enabled.\n\nDeprecated: Use SourceIntegrityResult for more detailed information. SignatureInfo will be removed with the next major version.",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceIntegrityResult": {
|
||||
"$ref": "#/definitions/v1alpha1SourceIntegrityCheckResult"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"title": "Tags specifies any tags currently attached to the revision\nFloating tags can move from one revision to another",
|
||||
|
|
@ -10637,6 +10646,94 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SourceIntegrity": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"git": {
|
||||
"$ref": "#/definitions/v1alpha1SourceIntegrityGit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SourceIntegrityCheckResult": {
|
||||
"description": "SourceIntegrityCheckResult represents a conclusion of the SourceIntegrity evaluation.\nEach check performed on a source(es), holds a check item representing all checks performed.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"checks": {
|
||||
"description": "Checks holds a list of checks performed, with their eventual problems. If a check is not specified here,\nit means it was not performed.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1SourceIntegrityCheckResultItem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SourceIntegrityCheckResultItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name of the check that is human-understandable pointing out to the kind of verification performed.",
|
||||
"type": "string"
|
||||
},
|
||||
"problems": {
|
||||
"description": "Problems is a list of messages explaining why the check failed. Empty list means the check has succeeded.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SourceIntegrityGit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"policies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1SourceIntegrityGitPolicy"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SourceIntegrityGitPolicy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"gpg": {
|
||||
"$ref": "#/definitions/v1alpha1SourceIntegrityGitPolicyGPG"
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"title": "List of repository criteria restricting repositories the policy will apply to",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1SourceIntegrityGitPolicyRepo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SourceIntegrityGitPolicyGPG": {
|
||||
"description": "SourceIntegrityGitPolicyGPG verifies that the commit(s) are both correctly signed by a key in the repo-server keyring,\nand that they are signed by one of the key listed in Keys.\n\nThis policy can be deactivated through the ARGOCD_GPG_ENABLED environment variable.\n\nNote the listing of problematic commits/signatures reported when \"strict\" mode validation fails may not be complete.\nThis means that a user that has addressed all problems reported by source integrity check can run into\nfurther problematic signatures on a subsequent attempt. That happens namely when history contains seal commits signed\nwith gpg keys that are in the keyring, but not listed in Keys.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keys": {
|
||||
"description": "List of key IDs to trust. The keys need to be in the repository server keyring.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SourceIntegrityGitPolicyRepo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"description": "URL specifier, glob.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SuccessfulHydrateOperation": {
|
||||
"type": "object",
|
||||
"title": "SuccessfulHydrateOperation contains information about the most recent successful hydrate operation",
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/util/cli"
|
||||
"github.com/argoproj/argo-cd/v3/util/env"
|
||||
"github.com/argoproj/argo-cd/v3/util/errors"
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg"
|
||||
"github.com/argoproj/argo-cd/v3/util/healthz"
|
||||
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
||||
"github.com/argoproj/argo-cd/v3/util/profile"
|
||||
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
|
||||
"github.com/argoproj/argo-cd/v3/util/tls"
|
||||
traceutil "github.com/argoproj/argo-cd/v3/util/trace"
|
||||
)
|
||||
|
|
@ -201,13 +201,13 @@ func NewCommand() *cobra.Command {
|
|||
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf("%s:%d", metricsHost, metricsPort), mux)) }()
|
||||
go func() { errors.CheckError(askPassServer.Run()) }()
|
||||
|
||||
if gpg.IsGPGEnabled() {
|
||||
if sourceintegrity.IsGPGEnabled() {
|
||||
log.Infof("Initializing GnuPG keyring at %s", common.GetGnuPGHomePath())
|
||||
err = gpg.InitializeGnuPG()
|
||||
err = sourceintegrity.InitializeGnuPG()
|
||||
errors.CheckError(err)
|
||||
|
||||
log.Infof("Populating GnuPG keyring with keys from %s", gnuPGSourcePath)
|
||||
added, removed, err := gpg.SyncKeyRingFromDirectory(gnuPGSourcePath)
|
||||
added, removed, err := sourceintegrity.SyncKeyRingFromDirectory(gnuPGSourcePath)
|
||||
errors.CheckError(err)
|
||||
log.Infof("Loaded %d (and removed %d) keys from keyring", len(added), len(removed))
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/util/cli"
|
||||
"github.com/argoproj/argo-cd/v3/util/errors"
|
||||
"github.com/argoproj/argo-cd/v3/util/git"
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg"
|
||||
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
||||
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
|
||||
"github.com/argoproj/argo-cd/v3/util/templates"
|
||||
)
|
||||
|
||||
|
|
@ -185,11 +185,12 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
|||
func NewProjectAddSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "add-signature-key PROJECT KEY-ID",
|
||||
Short: "Add GnuPG signature key to project",
|
||||
Short: "Add GnuPG signature key to project (DEPRECATED)",
|
||||
Example: templates.Examples(`
|
||||
# Add GnuPG signature key KEY-ID to project PROJECT
|
||||
argocd proj add-signature-key PROJECT KEY-ID
|
||||
`),
|
||||
Deprecated: "Managing project signature keys through CLI is deprecated, migrate to Source Integrity defined in AppProject manifest",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
ctx := c.Context()
|
||||
|
||||
|
|
@ -200,8 +201,9 @@ func NewProjectAddSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *c
|
|||
projName := args[0]
|
||||
signatureKey := args[1]
|
||||
|
||||
if !gpg.IsShortKeyID(signatureKey) && !gpg.IsLongKeyID(signatureKey) {
|
||||
log.Fatalf("%s is not a valid GnuPG key ID", signatureKey)
|
||||
_, err := sourceintegrity.KeyID(signatureKey)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
|
||||
|
|
@ -227,11 +229,12 @@ func NewProjectAddSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *c
|
|||
func NewProjectRemoveSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "remove-signature-key PROJECT KEY-ID",
|
||||
Short: "Remove GnuPG signature key from project",
|
||||
Short: "Remove GnuPG signature key from project (DEPRECATED)",
|
||||
Example: templates.Examples(`
|
||||
# Remove GnuPG signature key KEY-ID from project PROJECT
|
||||
argocd proj remove-signature-key PROJECT KEY-ID
|
||||
`),
|
||||
Deprecated: "Managing project signature keys through CLI is deprecated, migrate to Source Integrity defined in AppProject manifest",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
ctx := c.Context()
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/pkg/apis/application"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/config"
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg"
|
||||
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
|
||||
)
|
||||
|
||||
type ProjectOpts struct {
|
||||
|
|
@ -128,10 +128,11 @@ func (opts *ProjectOpts) GetDestinationServiceAccounts() []v1alpha1.ApplicationD
|
|||
func (opts *ProjectOpts) GetSignatureKeys() []v1alpha1.SignatureKey {
|
||||
signatureKeys := make([]v1alpha1.SignatureKey, 0)
|
||||
for _, keyStr := range opts.SignatureKeys {
|
||||
if !gpg.IsShortKeyID(keyStr) && !gpg.IsLongKeyID(keyStr) {
|
||||
log.Fatalf("'%s' is not a valid GnuPG key ID", keyStr)
|
||||
keyId, err := sourceintegrity.KeyID(keyStr)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
signatureKeys = append(signatureKeys, v1alpha1.SignatureKey{KeyID: gpg.KeyID(keyStr)})
|
||||
signatureKeys = append(signatureKeys, v1alpha1.SignatureKey{KeyID: keyId})
|
||||
}
|
||||
return signatureKeys
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1717,9 +1717,8 @@ func TestSetOperationStateOnDeletedApp(t *testing.T) {
|
|||
func TestSetOperationStateLogRetries(t *testing.T) {
|
||||
hook := utilTest.LogHook{}
|
||||
logrus.AddHook(&hook)
|
||||
t.Cleanup(func() {
|
||||
logrus.StandardLogger().ReplaceHooks(logrus.LevelHooks{})
|
||||
})
|
||||
t.Cleanup(hook.CleanupHook)
|
||||
|
||||
ctrl := newFakeController(t.Context(), &fakeData{apps: []runtime.Object{}}, nil)
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
fakeAppCs.ReactionChain = nil
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ func (ctrl *ApplicationController) executeHooks(hookType HookType, app *appv1.Ap
|
|||
}
|
||||
|
||||
// Fetch target objects from Git to know which hooks should exist
|
||||
targets, _, _, err := ctrl.appStateManager.GetRepoObjs(context.Background(), app, app.Spec.GetSources(), appLabelKey, revisions, false, false, false, proj, true)
|
||||
targets, _, _, err := ctrl.appStateManager.GetRepoObjs(context.Background(), app, app.Spec.GetSources(), appLabelKey, revisions, false, false, nil, proj, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func (ctrl *ApplicationController) GetRepoObjs(ctx context.Context, origApp *app
|
|||
delete(app.Annotations, appv1.AnnotationKeyManifestGeneratePaths)
|
||||
|
||||
// FIXME: use cache and revision cache
|
||||
objs, resp, _, err := ctrl.appStateManager.GetRepoObjs(ctx, app, drySources, appLabelKey, dryRevisions, true, true, false, project, false)
|
||||
objs, resp, _, err := ctrl.appStateManager.GetRepoObjs(ctx, app, drySources, appLabelKey, dryRevisions, true, true, nil, project, false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get repo objects: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ import (
|
|||
resourceutil "github.com/argoproj/argo-cd/gitops-engine/pkg/sync/resource"
|
||||
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync/syncwaves"
|
||||
kubeutil "github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
|
@ -41,7 +44,6 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
|
||||
appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/v3/util/db"
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg"
|
||||
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
"github.com/argoproj/argo-cd/v3/util/stats"
|
||||
|
|
@ -72,7 +74,7 @@ type managedResource struct {
|
|||
type AppStateManager interface {
|
||||
CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localObjects []string, hasMultipleSources bool) (*comparisonResult, error)
|
||||
SyncAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, state *v1alpha1.OperationState)
|
||||
GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error)
|
||||
GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error)
|
||||
}
|
||||
|
||||
// comparisonResult holds the state of an application after the reconciliation
|
||||
|
|
@ -128,7 +130,7 @@ type appStateManager struct {
|
|||
// task to the repo-server. It returns the list of generated manifests as unstructured
|
||||
// objects. It also returns the full response from all calls to the repo server as the
|
||||
// second argument.
|
||||
func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error) {
|
||||
func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error) {
|
||||
ts := stats.NewTimingStats()
|
||||
helmRepos, err := m.db.ListHelmRepositories(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -233,8 +235,9 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
|
|||
|
||||
keyManifestGenerateAnnotationVal, keyManifestGenerateAnnotationExists := app.Annotations[v1alpha1.AnnotationKeyManifestGeneratePaths]
|
||||
|
||||
sourceCount := len(sources)
|
||||
for i, source := range sources {
|
||||
if len(revisions) < len(sources) || revisions[i] == "" {
|
||||
if len(revisions) < sourceCount || revisions[i] == "" {
|
||||
revisions[i] = source.TargetRevision
|
||||
}
|
||||
repo, err := m.db.GetRepository(ctx, source.RepoURL, proj.Name)
|
||||
|
|
@ -242,6 +245,15 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
|
|||
return nil, nil, false, fmt.Errorf("failed to get repo %q: %w", source.RepoURL, err)
|
||||
}
|
||||
|
||||
syncedRevision := app.Status.Sync.Revision
|
||||
if app.Spec.HasMultipleSources() {
|
||||
if i < len(app.Status.Sync.Revisions) {
|
||||
syncedRevision = app.Status.Sync.Revisions[i]
|
||||
} else {
|
||||
syncedRevision = ""
|
||||
}
|
||||
}
|
||||
|
||||
revision := revisions[i]
|
||||
|
||||
appNamespace := app.Spec.Destination.Namespace
|
||||
|
|
@ -283,7 +295,7 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
|
|||
KustomizeOptions: kustomizeSettings,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: apiVersions,
|
||||
VerifySignature: verifySignature,
|
||||
SourceIntegrity: sourceIntegrity,
|
||||
HelmRepoCreds: helmRepoCreds,
|
||||
TrackingMethod: trackingMethod,
|
||||
EnabledSourceTypes: enabledSourceTypes,
|
||||
|
|
@ -296,7 +308,7 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
|
|||
InstallationID: installationID,
|
||||
})
|
||||
if err != nil {
|
||||
genErr := fmt.Errorf("failed to generate manifest for source %d of %d: %w", i+1, len(sources), err)
|
||||
genErr := fmt.Errorf("failed to generate manifest for source %d of %d: %w", i+1, sourceCount, err)
|
||||
if app.Spec.SourceHydrator != nil && app.Spec.SourceHydrator.HydrateTo != nil && strings.Contains(err.Error(), path.ErrMessageAppPathDoesNotExist) {
|
||||
genErr = fmt.Errorf("%w - waiting for an external process to update %s from %s", genErr, app.Spec.SourceHydrator.SyncSource.TargetBranch, app.Spec.SourceHydrator.HydrateTo.TargetBranch)
|
||||
}
|
||||
|
|
@ -305,10 +317,21 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
|
|||
|
||||
targetObj, err := unmarshalManifests(manifestInfo.Manifests)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to unmarshal manifests for source %d of %d: %w", i+1, len(sources), err)
|
||||
return nil, nil, false, fmt.Errorf("failed to unmarshal manifests for source %d of %d: %w", i+1, sourceCount, err)
|
||||
}
|
||||
targetObjs = append(targetObjs, targetObj...)
|
||||
manifestInfos = append(manifestInfos, manifestInfo)
|
||||
|
||||
// Update eventual check problems with the ID of the current source. This is so users can attribute problems to correct sources
|
||||
if sourceCount > 1 {
|
||||
var sourceId string
|
||||
if source.Name != "" {
|
||||
sourceId = "source " + source.Name
|
||||
} else {
|
||||
sourceId = fmt.Sprintf("source %d of %d", i+1, sourceCount)
|
||||
}
|
||||
manifestInfo.SourceIntegrityResult.InjectSourceName(sourceId)
|
||||
}
|
||||
}
|
||||
|
||||
ts.AddCheckpoint("manifests_ms")
|
||||
|
|
@ -503,45 +526,6 @@ func (m *appStateManager) getComparisonSettings() (string, map[string]v1alpha1.R
|
|||
return appLabelKey, resourceOverrides, resFilter, installationID, trackingMethod, nil
|
||||
}
|
||||
|
||||
// verifyGnuPGSignature verifies the result of a GnuPG operation for a given git
|
||||
// revision.
|
||||
func verifyGnuPGSignature(revision string, project *v1alpha1.AppProject, manifestInfo *apiclient.ManifestResponse) []v1alpha1.ApplicationCondition {
|
||||
now := metav1.Now()
|
||||
conditions := make([]v1alpha1.ApplicationCondition, 0)
|
||||
// We need to have some data in the verification result to parse, otherwise there was no signature
|
||||
if manifestInfo.VerifyResult != "" {
|
||||
verifyResult := gpg.ParseGitCommitVerification(manifestInfo.VerifyResult)
|
||||
switch verifyResult.Result {
|
||||
case gpg.VerifyResultGood:
|
||||
// This is the only case we allow to sync to, but we need to make sure signing key is allowed
|
||||
validKey := false
|
||||
for _, k := range project.Spec.SignatureKeys {
|
||||
if gpg.KeyID(k.KeyID) == gpg.KeyID(verifyResult.KeyID) && gpg.KeyID(k.KeyID) != "" {
|
||||
validKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validKey {
|
||||
msg := fmt.Sprintf("Found good signature made with %s key %s, but this key is not allowed in AppProject",
|
||||
verifyResult.Cipher, verifyResult.KeyID)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
|
||||
}
|
||||
case gpg.VerifyResultInvalid:
|
||||
msg := fmt.Sprintf("Found signature made with %s key %s, but verification result was invalid: '%s'",
|
||||
verifyResult.Cipher, verifyResult.KeyID, verifyResult.Message)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
|
||||
default:
|
||||
msg := fmt.Sprintf("Could not verify commit signature on revision '%s', check logs for more information.", revision)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
|
||||
}
|
||||
} else {
|
||||
msg := fmt.Sprintf("Target revision %s in Git is not signed, but a signature is required", revision)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
|
||||
}
|
||||
|
||||
return conditions
|
||||
}
|
||||
|
||||
func isManagedNamespace(ns *unstructured.Unstructured, app *v1alpha1.Application) bool {
|
||||
return ns != nil && ns.GetKind() == kubeutil.NamespaceKind && ns.GetName() == app.Spec.Destination.Namespace && app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.ManagedNamespaceMetadata != nil
|
||||
}
|
||||
|
|
@ -604,9 +588,6 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
|
|||
return &comparisonResult{syncStatus: syncStatus, healthStatus: health.HealthStatusUnknown}, nil
|
||||
}
|
||||
|
||||
// When signature keys are defined in the project spec, we need to verify the signature on the Git revision
|
||||
verifySignature := len(project.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled()
|
||||
|
||||
// do best effort loading live and target state to present as much information about app state as possible
|
||||
failedToLoadObjs := false
|
||||
conditions := make([]v1alpha1.ApplicationCondition, 0)
|
||||
|
|
@ -636,7 +617,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
|
|||
}
|
||||
}
|
||||
|
||||
targetObjs, manifestInfos, revisionsMayHaveChanges, err = m.GetRepoObjs(context.Background(), app, sources, appLabelKey, revisions, noCache, noRevisionCache, verifySignature, project, true)
|
||||
targetObjs, manifestInfos, revisionsMayHaveChanges, err = m.GetRepoObjs(context.Background(), app, sources, appLabelKey, revisions, noCache, noRevisionCache, project.EffectiveSourceIntegrity(), project, true)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
msg := "Failed to load target state: " + err.Error()
|
||||
|
|
@ -658,10 +639,10 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
|
|||
m.repoErrorCache.Delete(app.Name)
|
||||
}
|
||||
} else {
|
||||
// Prevent applying local manifests for now when signature verification is enabled
|
||||
// Prevent applying local manifests for now when source integrity is enforced
|
||||
// This is also enforced on API level, but as a last resort, we also enforce it here
|
||||
if gpg.IsGPGEnabled() && verifySignature {
|
||||
msg := "Cannot use local manifests when signature verification is required"
|
||||
if sourceintegrity.HasCriteria(project.EffectiveSourceIntegrity(), sources...) {
|
||||
msg := "Cannot use local manifests when source integrity is enforced"
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
|
|
@ -1003,12 +984,15 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
|
|||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: "error setting app health: " + err.Error(), LastTransitionTime: &now})
|
||||
}
|
||||
|
||||
// Git has already performed the signature verification via its GPG interface, and the result is available
|
||||
// in the manifest info received from the repository server. We now need to form our opinion about the result
|
||||
// and stop processing if we do not agree about the outcome.
|
||||
for _, manifestInfo := range manifestInfos {
|
||||
if gpg.IsGPGEnabled() && verifySignature && manifestInfo != nil {
|
||||
conditions = append(conditions, verifyGnuPGSignature(manifestInfo.Revision, project, manifestInfo)...)
|
||||
if manifestInfo != nil {
|
||||
if err = manifestInfo.SourceIntegrityResult.AsError(); err != nil {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{
|
||||
Type: v1alpha1.ApplicationConditionComparisonError,
|
||||
Message: err.Error(),
|
||||
LastTransitionTime: &now,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -1097,17 +1096,7 @@ func Test_appStateManager_persistRevisionHistory(t *testing.T) {
|
|||
assert.Empty(t, app.Status.History)
|
||||
}
|
||||
|
||||
// helper function to read contents of a file to string
|
||||
// panics on error
|
||||
func mustReadFile(path string) string {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
var signedProj = v1alpha1.AppProject{
|
||||
var projWithSourceIntegrity = v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
|
|
@ -1120,54 +1109,31 @@ var signedProj = v1alpha1.AppProject{
|
|||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
SignatureKeys: []v1alpha1.SignatureKey{
|
||||
{
|
||||
KeyID: "4AEE18F83AFDEB23",
|
||||
SourceIntegrity: &v1alpha1.SourceIntegrity{
|
||||
Git: &v1alpha1.SourceIntegrityGit{
|
||||
Policies: []*v1alpha1.SourceIntegrityGitPolicy{{
|
||||
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
|
||||
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeStrict,
|
||||
Keys: []string{"4AEE18F83AFDEB23"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSignedResponseNoSignatureRequired(t *testing.T) {
|
||||
func TestNoSourceIntegrity(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
|
||||
// We have a good signature response, but project does not require signed commits
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(t.Context(), &data, nil)
|
||||
sources := make([]v1alpha1.ApplicationSource, 0)
|
||||
sources = append(sources, app.Spec.GetSource())
|
||||
revisions := make([]string, 0)
|
||||
revisions = append(revisions, "")
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Empty(t, compRes.resources)
|
||||
assert.Empty(t, compRes.managedResources)
|
||||
assert.Empty(t, app.Status.Conditions)
|
||||
}
|
||||
// We have a bad signature response, but project does not require signed commits
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
SourceIntegrityResult: nil, // No verification requested
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
|
|
@ -1187,19 +1153,22 @@ func TestSignedResponseNoSignatureRequired(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSignedResponseSignatureRequired(t *testing.T) {
|
||||
func TestValidSourceIntegrity(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
|
||||
// We have a good signature response, valid key, and signing is required - sync!
|
||||
// Source integrity required, and it is valid - sync!
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
|
||||
Name: "Some/check",
|
||||
Problems: []string{}, // Valid
|
||||
}}},
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
|
|
@ -1208,7 +1177,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
|
|||
sources = append(sources, app.Spec.GetSource())
|
||||
revisions := make([]string, 0)
|
||||
revisions = append(revisions, "")
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, nil, false)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
|
|
@ -1217,16 +1186,19 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
|
|||
assert.Empty(t, compRes.managedResources)
|
||||
assert.Empty(t, app.Status.Conditions)
|
||||
}
|
||||
// We have a bad signature response and signing is required - do not sync
|
||||
// Source integrity required, not valid - do not sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
|
||||
Name: "Some/check",
|
||||
Problems: []string{"The thing have failed to validate!"},
|
||||
}}},
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
|
|
@ -1235,170 +1207,64 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
|
|||
sources = append(sources, app.Spec.GetSource())
|
||||
revisions := make([]string, 0)
|
||||
revisions = append(revisions, "abc123")
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, nil, false)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Empty(t, compRes.resources)
|
||||
assert.Empty(t, compRes.managedResources)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
}
|
||||
// We have a malformed signature response and signing is required - do not sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_malformed1.txt"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(t.Context(), &data, nil)
|
||||
sources := make([]v1alpha1.ApplicationSource, 0)
|
||||
sources = append(sources, app.Spec.GetSource())
|
||||
revisions := make([]string, 0)
|
||||
revisions = append(revisions, "abc123")
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Empty(t, compRes.resources)
|
||||
assert.Empty(t, compRes.managedResources)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
}
|
||||
// We have no signature response (no signature made) and signing is required - do not sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: "",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(t.Context(), &data, nil)
|
||||
sources := make([]v1alpha1.ApplicationSource, 0)
|
||||
sources = append(sources, app.Spec.GetSource())
|
||||
revisions := make([]string, 0)
|
||||
revisions = append(revisions, "abc123")
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Empty(t, compRes.resources)
|
||||
assert.Empty(t, compRes.managedResources)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
require.Len(t, app.Status.Conditions, 1)
|
||||
assert.Contains(t, app.Status.Conditions[0].Message, "Some/check: The thing have failed to validate!")
|
||||
}
|
||||
|
||||
// We have a good signature and signing is required, but key is not allowed - do not sync
|
||||
// Source integrity required, unknown key - do not sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
|
||||
Name: "Some/check",
|
||||
Problems: []string{"The thing have failed to validate!"},
|
||||
}}},
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(t.Context(), &data, nil)
|
||||
testProj := signedProj
|
||||
testProj.Spec.SignatureKeys[0].KeyID = "4AEE18F83AFDEB24"
|
||||
sources := make([]v1alpha1.ApplicationSource, 0)
|
||||
sources = append(sources, app.Spec.GetSource())
|
||||
revisions := make([]string, 0)
|
||||
revisions = append(revisions, "abc123")
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &testProj, revisions, sources, false, false, nil, false)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, nil, false)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Empty(t, compRes.resources)
|
||||
assert.Empty(t, compRes.managedResources)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Contains(t, app.Status.Conditions[0].Message, "key is not allowed")
|
||||
}
|
||||
// Signature required and local manifests supplied - do not sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: "",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
// it doesn't matter for our test whether local manifests are valid
|
||||
localManifests := []string{"foobar"}
|
||||
ctrl := newFakeController(t.Context(), &data, nil)
|
||||
sources := make([]v1alpha1.ApplicationSource, 0)
|
||||
sources = append(sources, app.Spec.GetSource())
|
||||
revisions := make([]string, 0)
|
||||
revisions = append(revisions, "abc123")
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, localManifests, false)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, v1alpha1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
|
||||
assert.Empty(t, compRes.resources)
|
||||
assert.Empty(t, compRes.managedResources)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Contains(t, app.Status.Conditions[0].Message, "Cannot use local manifests")
|
||||
require.Len(t, app.Status.Conditions, 1)
|
||||
assert.Contains(t, app.Status.Conditions[0].Message, "The thing have failed to validate!")
|
||||
}
|
||||
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "false")
|
||||
// We have a bad signature response and signing would be required, but GPG subsystem is disabled - sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(t.Context(), &data, nil)
|
||||
sources := make([]v1alpha1.ApplicationSource, 0)
|
||||
sources = append(sources, app.Spec.GetSource())
|
||||
revisions := make([]string, 0)
|
||||
revisions = append(revisions, "abc123")
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Empty(t, compRes.resources)
|
||||
assert.Empty(t, compRes.managedResources)
|
||||
assert.Empty(t, app.Status.Conditions)
|
||||
}
|
||||
|
||||
// Signature required and local manifests supplied and GPG subsystem is disabled - sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: "",
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
|
||||
Name: "Some/check",
|
||||
Problems: []string{"The thing have failed to validate!"},
|
||||
}}},
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
|
|
@ -1409,7 +1275,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
|
|||
sources = append(sources, app.Spec.GetSource())
|
||||
revisions := make([]string, 0)
|
||||
revisions = append(revisions, "abc123")
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, localManifests, false)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, localManifests, false)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
|
|
@ -1637,11 +1503,10 @@ func TestUseDiffCache(t *testing.T) {
|
|||
"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-svc\",\"namespace\":\"httpbin\"},\"spec\":{\"ports\":[{\"name\":\"http-port\",\"port\":7777,\"targetPort\":80},{\"name\":\"test\",\"port\":333}],\"selector\":{\"app\":\"httpbin\"}}}",
|
||||
"{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-deployment\",\"namespace\":\"httpbin\"},\"spec\":{\"replicas\":2,\"selector\":{\"matchLabels\":{\"app\":\"httpbin\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"httpbin\"}},\"spec\":{\"containers\":[{\"image\":\"kennethreitz/httpbin\",\"imagePullPolicy\":\"Always\",\"name\":\"httpbin\",\"ports\":[{\"containerPort\":80}]}]}}}}",
|
||||
},
|
||||
Namespace: "",
|
||||
Server: "",
|
||||
Revision: revision,
|
||||
SourceType: "Kustomize",
|
||||
VerifyResult: "",
|
||||
Namespace: "",
|
||||
Server: "",
|
||||
Revision: revision,
|
||||
SourceType: "Kustomize",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1983,7 +1848,7 @@ func TestCompareAppState_CallUpdateRevisionForPaths_ForOCI(t *testing.T) {
|
|||
sources := make([]v1alpha1.ApplicationSource, 0)
|
||||
sources = append(sources, source)
|
||||
|
||||
_, _, revisionsMayHaveChanges, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "abc123", []string{"123456"}, false, false, false, &defaultProj, false)
|
||||
_, _, revisionsMayHaveChanges, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "abc123", []string{"123456"}, false, false, defaultProj.EffectiveSourceIntegrity(), &defaultProj, false)
|
||||
require.NoError(t, err)
|
||||
require.False(t, revisionsMayHaveChanges)
|
||||
}
|
||||
|
|
@ -2037,7 +1902,7 @@ func TestCompareAppState_CallUpdateRevisionForPaths_ForMultiSource(t *testing.T)
|
|||
|
||||
sources := app.Spec.Sources
|
||||
|
||||
_, _, revisionsMayHaveChanges, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "0.0.1", revisions, false, false, false, &defaultProj, false)
|
||||
_, _, revisionsMayHaveChanges, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "0.0.1", revisions, false, false, defaultProj.EffectiveSourceIntegrity(), &defaultProj, false)
|
||||
require.NoError(t, err)
|
||||
require.False(t, revisionsMayHaveChanges)
|
||||
}
|
||||
|
|
@ -2067,7 +1932,7 @@ func Test_GetRepoObjs_HydrateToAppPathNotExist(t *testing.T) {
|
|||
ctrl := newFakeController(t.Context(), &fakeData{manifestResponse: &apiclient.ManifestResponse{}}, errors.New("env/prod/my-app: app path does not exist"))
|
||||
source := app.Spec.GetSource()
|
||||
|
||||
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, []v1alpha1.ApplicationSource{source}, "app", []string{""}, true, false, false, &defaultProj, false)
|
||||
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, []v1alpha1.ApplicationSource{source}, "app", []string{""}, true, false, nil, &defaultProj, false)
|
||||
require.ErrorContains(t, err, "app path does not exist")
|
||||
require.ErrorContains(t, err, "waiting for an external process to update env/prod from env/prod-next")
|
||||
})
|
||||
|
|
@ -2091,7 +1956,7 @@ func Test_GetRepoObjs_HydrateToAppPathNotExist(t *testing.T) {
|
|||
ctrl := newFakeController(t.Context(), &fakeData{manifestResponse: &apiclient.ManifestResponse{}}, errors.New("env/prod/my-app: app path does not exist"))
|
||||
source := app.Spec.GetSource()
|
||||
|
||||
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, []v1alpha1.ApplicationSource{source}, "app", []string{""}, true, false, false, &defaultProj, false)
|
||||
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, []v1alpha1.ApplicationSource{source}, "app", []string{""}, true, false, nil, &defaultProj, false)
|
||||
require.ErrorContains(t, err, "app path does not exist")
|
||||
require.NotContains(t, err.Error(), "waiting for an external process")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -169,11 +169,14 @@ func TestSyncComparisonError(t *testing.T) {
|
|||
data := fakeData{
|
||||
apps: []runtime.Object{app, defaultProject},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: "something went wrong",
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
|
||||
Name: "GIT/GPG",
|
||||
Problems: []string{"Unknown key 'XXX'"},
|
||||
}}},
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,3 +117,16 @@ spec:
|
|||
# Applications to reside in. Details: https://argo-cd.readthedocs.io/en/stable/operator-manual/app-any-namespace/
|
||||
sourceNamespaces:
|
||||
- "argocd-apps-*"
|
||||
|
||||
# Source Integrity declares criteria for application source repositories, such as cryptographic signing, etc.
|
||||
# https://argo-cd.readthedocs.io/en/latest/user-guide/source-integrity/
|
||||
sourceIntegrity:
|
||||
git:
|
||||
policies:
|
||||
- repos:
|
||||
- url: 'https://github.com/foo/*'
|
||||
- url: '!https://github.com/foo/bar.git'
|
||||
gpg:
|
||||
mode: strict
|
||||
keys:
|
||||
- "D56C4FCA57A46444"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ data:
|
|||
|
||||
## New RBAC rules for GnuPG related features
|
||||
|
||||
The [GnuPG feature](../../user-guide/gpg-verification.md) has introduced a new
|
||||
The [GnuPG feature](../../user-guide/source-integrity-git-gpg) has introduced a new
|
||||
RBAC resource in Argo CD, `gpgkeys`.
|
||||
|
||||
Please adapt your RBAC rules with the appropriate permissions. The least set of
|
||||
|
|
@ -40,6 +40,6 @@ p, <your-role>, gpgkeys, get, *, allow
|
|||
```
|
||||
|
||||
More information can be found in the
|
||||
[documentation](../../user-guide/gpg-verification.md#rbac-rules-for-managing-gnupg-keys)
|
||||
[documentation](../../user-guide/source-integrity-git-gpg#keyring-rbac-rules).
|
||||
|
||||
From here on you can follow the [regular upgrade process](./overview.md).
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ In fact, it does not harm UX *and* improves security with:
|
|||
#### Git history *sealing* for strict verification mode
|
||||
|
||||
A sealing commit is a gpg signed commit that works as a "seal of approval" attesting that all its ancestor commits were either signed by a trusted key, or reviewed and trusted by the commit author.
|
||||
Argo CD verifying gpg signatures would then progres only as far back in the history as the most recent "seal" commits in each individual ancestral branch.
|
||||
Argo CD verifying gpg signatures would then progress only as far back in the history as the most recent "seal" commits in each individual ancestral branch.
|
||||
|
||||
In practice, a commiter reviews all commits that are not signed or signed with untrusted keys from the previous "seal" and creates a (possibly empty) commit with a custom trailer.
|
||||
Such commits can have the following organization level semantics:
|
||||
|
|
|
|||
2
docs/user-guide/commands/argocd_proj.md
generated
2
docs/user-guide/commands/argocd_proj.md
generated
|
|
@ -86,7 +86,6 @@ argocd proj [flags]
|
|||
* [argocd proj add-destination](argocd_proj_add-destination.md) - Add project destination
|
||||
* [argocd proj add-destination-service-account](argocd_proj_add-destination-service-account.md) - Add project destination's default service account
|
||||
* [argocd proj add-orphaned-ignore](argocd_proj_add-orphaned-ignore.md) - Add a resource to orphaned ignore list
|
||||
* [argocd proj add-signature-key](argocd_proj_add-signature-key.md) - Add GnuPG signature key to project
|
||||
* [argocd proj add-source](argocd_proj_add-source.md) - Add project source repository
|
||||
* [argocd proj add-source-namespace](argocd_proj_add-source-namespace.md) - Add source namespace to the AppProject
|
||||
* [argocd proj allow-cluster-resource](argocd_proj_allow-cluster-resource.md) - Adds a cluster-scoped API resource to the allow list and removes it from deny list
|
||||
|
|
@ -101,7 +100,6 @@ argocd proj [flags]
|
|||
* [argocd proj remove-destination](argocd_proj_remove-destination.md) - Remove project destination
|
||||
* [argocd proj remove-destination-service-account](argocd_proj_remove-destination-service-account.md) - Remove default destination service account from the project
|
||||
* [argocd proj remove-orphaned-ignore](argocd_proj_remove-orphaned-ignore.md) - Remove a resource from orphaned ignore list
|
||||
* [argocd proj remove-signature-key](argocd_proj_remove-signature-key.md) - Remove GnuPG signature key from project
|
||||
* [argocd proj remove-source](argocd_proj_remove-source.md) - Remove project source repository
|
||||
* [argocd proj remove-source-namespace](argocd_proj_remove-source-namespace.md) - Removes the source namespace from the AppProject
|
||||
* [argocd proj role](argocd_proj_role.md) - Manage a project's roles
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## argocd proj add-signature-key
|
||||
|
||||
Add GnuPG signature key to project
|
||||
Add GnuPG signature key to project (DEPRECATED)
|
||||
|
||||
```
|
||||
argocd proj add-signature-key PROJECT KEY-ID [flags]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## argocd proj remove-signature-key
|
||||
|
||||
Remove GnuPG signature key from project
|
||||
Remove GnuPG signature key from project (DEPRECATED)
|
||||
|
||||
```
|
||||
argocd proj remove-signature-key PROJECT KEY-ID [flags]
|
||||
|
|
|
|||
|
|
@ -1,335 +1,6 @@
|
|||
# GnuPG signature verification
|
||||
|
||||
## Overview
|
||||
|
||||
As of v1.7 it is possible to configure ArgoCD to only sync against commits
|
||||
that are signed in Git using GnuPG. Signature verification is configured on
|
||||
project level.
|
||||
|
||||
If a project is configured to enforce signature verification, all applications
|
||||
associated with this project must have the commits in the source repositories
|
||||
signed with a GnuPG public key known to ArgoCD. ArgoCD will refuse to sync to
|
||||
any revision that does not have a valid signature made by one of the configured
|
||||
keys. The controller will emit a `ResourceComparison` error if it tries to sync
|
||||
to a revision that is either not signed, or is signed by an unknown or not
|
||||
allowed public key.
|
||||
|
||||
By default, signature verification is enabled but not enforced. If you wish to
|
||||
completely disable the GnuPG functionality in ArgoCD, you have to set the
|
||||
environment variable `ARGOCD_GPG_ENABLED` to `"false"` in the pod templates of
|
||||
the `argocd-server`, `argocd-repo-server`, `argocd-application-controller` and
|
||||
`argocd-applicationset-controller` deployment manifests.
|
||||
|
||||
Verification of GnuPG signatures is only supported with Git repositories. It is
|
||||
not possible using Helm repositories.
|
||||
|
||||
> [!NOTE]
|
||||
> **A few words about trust**
|
||||
> **GPG Signature verification is deprecated and will be removed in the next major Argo CD version.**
|
||||
>
|
||||
> ArgoCD uses a very simple trust model for the keys you import: Once the key
|
||||
> is imported, ArgoCD will trust it. ArgoCD does not support more complex
|
||||
> trust models, and it is not necessary (nor possible) to sign the public keys
|
||||
> you are going to import into ArgoCD.
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
> Signature verification is not supported for the templated `project` field when
|
||||
> using the Git generator.
|
||||
|
||||
## Signature verification targets
|
||||
|
||||
If signature verification is enforced, ArgoCD will verify the signature using
|
||||
following strategy:
|
||||
|
||||
* If `target revision` is a pointer to a commit object (i.e. a branch name, the
|
||||
name of a reference such as `HEAD` or a commit SHA), ArgoCD will perform the
|
||||
signature verification on the commit object the name points to, i.e. a commit.
|
||||
|
||||
* If `target revision` resolves to a tag and the tag is a lightweight tag, the
|
||||
behaviour is same as if `target revision` would be a pointer to a commit
|
||||
object. However, if the tag is annotated, the target revision will point to
|
||||
a *tag* object and thus, the signature verification is performed on the tag
|
||||
object, i.e. the tag itself must be signed (using `git tag -s`).
|
||||
|
||||
## Enforcing signature verification
|
||||
|
||||
To configure enforcing of signature verification, the following steps must be
|
||||
performed:
|
||||
|
||||
* Import the GnuPG public key(s) used for signing commits in ArgoCD
|
||||
* Configure a project to enforce signature verification for given keys
|
||||
|
||||
Once you have configured one or more keys to be required for verification for
|
||||
a given project, enforcement is active for all applications associated with
|
||||
this project.
|
||||
|
||||
> [!WARNING]
|
||||
> If signature verification is enforced, you will not be able to sync from
|
||||
> local sources (i.e. `argocd app sync --local`) anymore.
|
||||
|
||||
## RBAC rules for managing GnuPG keys
|
||||
|
||||
The appropriate resource notation for Argo CD's RBAC implementation to allow
|
||||
the managing of GnuPG keys is `gpgkeys`.
|
||||
|
||||
To allow listing of keys for a role named `role:myrole`, use:
|
||||
|
||||
```
|
||||
p, role:myrole, gpgkeys, get, *, allow
|
||||
```
|
||||
|
||||
To allow adding keys for a role named `role:myrole`, use:
|
||||
|
||||
```
|
||||
p, role:myrole, gpgkeys, create, *, allow
|
||||
```
|
||||
|
||||
And finally, to allow deletion of keys for a role named `role:myrole`, use:
|
||||
|
||||
```
|
||||
p, role:myrole, gpgkeys, delete, *, allow
|
||||
```
|
||||
|
||||
## Importing GnuPG public keys
|
||||
|
||||
You can configure the GnuPG public keys that ArgoCD will use for verification
|
||||
of commit signatures using either the CLI, the web UI or configuring it using
|
||||
declarative setup.
|
||||
|
||||
> [!NOTE]
|
||||
> After you have imported a GnuPG key, it may take a while until the key is
|
||||
> propagated within the cluster, even if listed as configured. If you still
|
||||
> cannot sync to commits signed by the already imported key, please see the
|
||||
> troubleshooting section below.
|
||||
|
||||
Users wanting to manage the GnuPG public key configuration require the RBAC
|
||||
permissions for `gpgkeys` resources.
|
||||
|
||||
### Manage public keys using the CLI
|
||||
|
||||
To configure GnuPG public keys using the CLI, use the `argocd gpg` command.
|
||||
|
||||
#### Listing all configured keys
|
||||
|
||||
To list all configured keys known to ArgoCD, use the `argocd gpg list`
|
||||
sub-command:
|
||||
|
||||
```bash
|
||||
argocd gpg list
|
||||
```
|
||||
|
||||
#### Show information about a certain key
|
||||
|
||||
To get information about a specific key, use the `argocd gpg get` sub-command:
|
||||
|
||||
```bash
|
||||
argocd gpg get <key-id>
|
||||
```
|
||||
|
||||
#### Importing a key
|
||||
|
||||
To import a new key to ArgoCD, use the `argocd gpg add` sub-command:
|
||||
|
||||
```bash
|
||||
argocd gpg add --from <path-to-key>
|
||||
```
|
||||
|
||||
The key to be imported can be either in binary or ASCII-armored format.
|
||||
|
||||
#### Removing a key from configuration
|
||||
|
||||
To remove a previously configured key from the configuration, use the
|
||||
`argocd gpg rm` sub-command:
|
||||
|
||||
```bash
|
||||
argocd gpg rm <key-id>
|
||||
```
|
||||
|
||||
### Manage public keys using the Web UI
|
||||
|
||||
Basic key management functionality for listing, importing and removing GnuPG
|
||||
public keys is implemented in the Web UI. You can find the configuration
|
||||
module from the **Settings** page in the **GnuPG keys** module.
|
||||
|
||||
Please note that when you configure keys using the Web UI, the key must be
|
||||
imported in ASCII armored format for now.
|
||||
|
||||
### Manage public keys in declarative setup
|
||||
|
||||
ArgoCD stores public keys internally in the `argocd-gpg-keys-cm` ConfigMap
|
||||
resource, with the public GnuPG key's ID as its name and the ASCII armored
|
||||
key data as string value, i.e. the entry for the GitHub's web-flow signing
|
||||
key would look like follows:
|
||||
|
||||
```yaml
|
||||
4AEE18F83AFDEB23: |
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBFmUaEEBCACzXTDt6ZnyaVtueZASBzgnAmK13q9Urgch+sKYeIhdymjuMQta
|
||||
x15OklctmrZtqre5kwPUosG3/B2/ikuPYElcHgGPL4uL5Em6S5C/oozfkYzhwRrT
|
||||
SQzvYjsE4I34To4UdE9KA97wrQjGoz2Bx72WDLyWwctD3DKQtYeHXswXXtXwKfjQ
|
||||
7Fy4+Bf5IPh76dA8NJ6UtjjLIDlKqdxLW4atHe6xWFaJ+XdLUtsAroZcXBeWDCPa
|
||||
buXCDscJcLJRKZVc62gOZXXtPfoHqvUPp3nuLA4YjH9bphbrMWMf810Wxz9JTd3v
|
||||
yWgGqNY0zbBqeZoGv+TuExlRHT8ASGFS9SVDABEBAAG0NUdpdEh1YiAod2ViLWZs
|
||||
b3cgY29tbWl0IHNpZ25pbmcpIDxub3JlcGx5QGdpdGh1Yi5jb20+iQEiBBMBCAAW
|
||||
BQJZlGhBCRBK7hj4Ov3rIwIbAwIZAQAAmQEH/iATWFmi2oxlBh3wAsySNCNV4IPf
|
||||
DDMeh6j80WT7cgoX7V7xqJOxrfrqPEthQ3hgHIm7b5MPQlUr2q+UPL22t/I+ESF6
|
||||
9b0QWLFSMJbMSk+BXkvSjH9q8jAO0986/pShPV5DU2sMxnx4LfLfHNhTzjXKokws
|
||||
+8ptJ8uhMNIDXfXuzkZHIxoXk3rNcjDN5c5X+sK8UBRH092BIJWCOfaQt7v7wig5
|
||||
4Ra28pM9GbHKXVNxmdLpCFyzvyMuCmINYYADsC848QQFFwnd4EQnupo6QvhEVx1O
|
||||
j7wDwvuH5dCrLuLwtwXaQh0onG4583p0LGms2Mf5F+Ick6o/4peOlBoZz48=
|
||||
=Bvzs
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
```
|
||||
|
||||
## Configuring a project to enforce signature verification
|
||||
|
||||
Once you have imported the GnuPG keys to ArgoCD, you must now configure the
|
||||
project to enforce the verification of commit signatures with the imported
|
||||
keys.
|
||||
|
||||
### Configuring using the CLI
|
||||
|
||||
#### Adding a key ID to list of allowed keys
|
||||
|
||||
To add a key ID to the list of allowed GnuPG keys for a project, you can use
|
||||
the `argocd proj add-signature-key` command, i.e. the following command would
|
||||
add the key ID `4AEE18F83AFDEB23` to the project named `myproj`:
|
||||
|
||||
```bash
|
||||
argocd proj add-signature-key myproj 4AEE18F83AFDEB23
|
||||
```
|
||||
|
||||
#### Removing a key ID from the list of allowed keys
|
||||
|
||||
Similarly, you can remove a key ID from the list of allowed GnuPG keys for a
|
||||
project using the `argocd proj remove-signature-key` command, i.e. to remove
|
||||
the key added above from project `myproj`, use the command:
|
||||
|
||||
```bash
|
||||
argocd proj remove-signature-key myproj 4AEE18F83AFDEB23
|
||||
```
|
||||
|
||||
#### Showing allowed key IDs for a project
|
||||
|
||||
To see which key IDs are allowed for a given project, you can inspect the
|
||||
output of the `argocd proj get` command, i.e. for a project named `gpg`:
|
||||
|
||||
```bash
|
||||
$ argocd proj get gpg
|
||||
Name: gpg
|
||||
Description: GnuPG verification
|
||||
Destinations: *,*
|
||||
Repositories: *
|
||||
Allowed Cluster Resources: */*
|
||||
Denied Namespaced Resources: <none>
|
||||
Signature keys: 4AEE18F83AFDEB23, 07E34825A909B250
|
||||
Orphaned Resources: disabled
|
||||
```
|
||||
|
||||
#### Override list of key IDs
|
||||
|
||||
You can also explicitly set the currently allowed keys with one or more new keys
|
||||
using the `argocd proj set` command in combination with the `--signature-keys`
|
||||
flag, which you can use to specify a comma separated list of allowed key IDs:
|
||||
|
||||
```bash
|
||||
argocd proj set myproj --signature-keys 4AEE18F83AFDEB23,07E34825A909B250
|
||||
```
|
||||
|
||||
The `--signature-keys` flag can also be used on project creation, i.e. the
|
||||
`argocd proj create` command.
|
||||
|
||||
### Configure using the Web UI
|
||||
|
||||
You can configure the GnuPG key IDs required for signature verification using
|
||||
the web UI, in the Project configuration. Navigate to the **Settings** page
|
||||
and select the **Projects** module, then click on the project you want to
|
||||
configure.
|
||||
|
||||
From the project's details page, click **Edit** and find the
|
||||
**Required signature keys** section, where you can add or remove the key IDs
|
||||
for signature verification. After you have modified your project, click
|
||||
**Update** to save the changes.
|
||||
|
||||
### Configure using declarative setup
|
||||
|
||||
You can specify the key IDs required for signature verification in the project
|
||||
manifest within the `signatureKeys` section, i.e:
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: AppProject
|
||||
metadata:
|
||||
name: gpg
|
||||
namespace: argocd
|
||||
spec:
|
||||
clusterResourceWhitelist:
|
||||
- group: '*'
|
||||
kind: '*'
|
||||
description: GnuPG verification
|
||||
destinations:
|
||||
- namespace: '*'
|
||||
server: '*'
|
||||
namespaceResourceWhitelist:
|
||||
- group: '*'
|
||||
kind: '*'
|
||||
signatureKeys:
|
||||
- keyID: 4AEE18F83AFDEB23
|
||||
sourceRepos:
|
||||
- '*'
|
||||
```
|
||||
|
||||
`signatureKeys` is an array of `SignatureKey` objects, whose only property is
|
||||
`keyID` at the moment.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Disabling the feature
|
||||
|
||||
The GnuPG feature can be completely disabled if desired. In order to disable it,
|
||||
set the environment variable `ARGOCD_GPG_ENABLED` to `false` for the pod
|
||||
templates of the `argocd-server`, `argocd-repo-server`, `argocd-application-controller`
|
||||
and `argocd-applicationset-controller` deployments.
|
||||
|
||||
After the pods have been restarted, the GnuPG feature is disabled.
|
||||
|
||||
### GnuPG key ring
|
||||
|
||||
The GnuPG key ring used for signature verification is maintained within the
|
||||
pods of `argocd-repo-server`. The keys in the keyring are synchronized to the
|
||||
configuration stored in the `argocd-gpg-keys-cm` ConfigMap resource, which is
|
||||
volume-mounted to the `argocd-repo-server` pods.
|
||||
|
||||
> [!NOTE]
|
||||
> The GnuPG key ring in the pods is transient and gets recreated from the
|
||||
> configuration on each restart of the pods. You should never add or remove
|
||||
> keys manually to the key ring, because your changes will be lost. Also,
|
||||
> any of the private keys found in the key ring are transient and will be
|
||||
> regenerated upon each restart. The private key is only used to build the
|
||||
> trust DB for the running pod.
|
||||
|
||||
To check whether the keys are actually in sync, you can `kubectl exec` into the
|
||||
repository server's pods and inspect the key ring, which is located at path
|
||||
`/app/config/gpg/keys`
|
||||
|
||||
```bash
|
||||
$ kubectl exec -it argocd-repo-server-7d6bdfdf6d-hzqkg bash
|
||||
argocd@argocd-repo-server-7d6bdfdf6d-hzqkg:~$ GNUPGHOME=/app/config/gpg/keys gpg --list-keys
|
||||
/app/config/gpg/keys/pubring.kbx
|
||||
--------------------------------
|
||||
pub rsa2048 2020-06-15 [SC] [expires: 2020-12-12]
|
||||
D48F075D818A813C436914BC9324F0D2144753B1
|
||||
uid [ultimate] Anon Ymous (ArgoCD key signing key) <noreply@argoproj.io>
|
||||
|
||||
pub rsa2048 2017-08-16 [SC]
|
||||
5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB23
|
||||
uid [ultimate] GitHub (web-flow commit signing) <noreply@github.com>
|
||||
|
||||
argocd@argocd-repo-server-7d6bdfdf6d-hzqkg:~$
|
||||
```
|
||||
|
||||
If the key ring stays out of sync with your configuration after you have added
|
||||
or removed keys for a longer period of time, you might want to restart your
|
||||
`argocd-repo-server` pods. If such a problem persists, please consider raising
|
||||
a bug report.
|
||||
> Consult [Source Integrity Verification](./source-integrity.md) for the new and expanded alternative.
|
||||
|
|
|
|||
373
docs/user-guide/source-integrity-git-gpg.md
Normal file
373
docs/user-guide/source-integrity-git-gpg.md
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
# Git GnuPG signature verification
|
||||
|
||||
## Overview
|
||||
|
||||
Verify that commits in the source repository are correctly signed with one of the blessed GnuPG keys.
|
||||
|
||||
> [!NOTE]
|
||||
> **A few words about trust**
|
||||
>
|
||||
> ArgoCD uses a very simple trust model for the keys you import: Once the key
|
||||
> is imported, ArgoCD will trust it. ArgoCD does not support more complex
|
||||
> trust models, and it is not necessary (nor possible) to sign the public keys
|
||||
> you are going to import into ArgoCD.
|
||||
|
||||
> [!NOTE]
|
||||
> **Compatibility notice**
|
||||
>
|
||||
> The GnuPG verification was first introduced in v1.7 as a project-wide constraint configured by `signatureKeys`.
|
||||
> As of v**TODO**, it is supported as one of the methods for source integrity verification, but it is using a different declaration format.
|
||||
> Keys configured in `signatureKeys` will continue to be supported, but they cannot be used together with `sourceIntegrity`.
|
||||
> See below on how to convert the legacy `signatureKeys` configuration to `sourceIntegrity`.
|
||||
|
||||
Verification of GnuPG signatures is only supported with Git repositories. It is
|
||||
not possible when using Helm or OCI application sources.
|
||||
|
||||
The GnuPG verification requires populating the Argo CD GnuPG keyring, and configuring source integrity policies for your repositories.
|
||||
|
||||
## Managing Argo CD GnuPG keyring
|
||||
|
||||
All the GnuPG keys Argo CD is going to trust must be introduced in its keyring first.
|
||||
|
||||
### Keyring RBAC rules
|
||||
|
||||
The appropriate resource notation for Argo CD's RBAC implementation to allow
|
||||
the managing of GnuPG keys is `gpgkeys`.
|
||||
|
||||
To allow *listing* of keys for a role named `role:myrole`, use:
|
||||
|
||||
```
|
||||
p, role:myrole, gpgkeys, get, *, allow
|
||||
```
|
||||
|
||||
To allow *adding* keys for a role named `role:myrole`, use:
|
||||
|
||||
```
|
||||
p, role:myrole, gpgkeys, create, *, allow
|
||||
```
|
||||
|
||||
And finally, to allow *deletion* of keys for a role named `role:myrole`, use:
|
||||
|
||||
```
|
||||
p, role:myrole, gpgkeys, delete, *, allow
|
||||
```
|
||||
|
||||
### Keyring management
|
||||
|
||||
You can configure the GnuPG public keys that ArgoCD will use for verification
|
||||
of commit signatures using either the CLI, the web UI or configuring it using
|
||||
declarative setup.
|
||||
|
||||
> [!NOTE]
|
||||
> After you have imported a GnuPG key, it may take a while until the key is
|
||||
> propagated within the cluster, even if listed as configured. If you still
|
||||
> cannot sync to commits signed by the already imported key, please see the
|
||||
> troubleshooting section below.
|
||||
|
||||
#### Manage public keys using the CLI
|
||||
|
||||
To configure GnuPG public keys using the CLI, use the `argocd gpg` command.
|
||||
|
||||
##### Listing all configured keys
|
||||
|
||||
To list all configured keys known to ArgoCD, use the `argocd gpg list`
|
||||
sub-command:
|
||||
|
||||
```bash
|
||||
argocd gpg list
|
||||
```
|
||||
|
||||
##### Show information about a certain key
|
||||
|
||||
To get information about a specific key, use the `argocd gpg get` sub-command:
|
||||
|
||||
```bash
|
||||
argocd gpg get <key-id>
|
||||
```
|
||||
|
||||
##### Importing a key
|
||||
|
||||
To import a new *public* key to ArgoCD, use the `argocd gpg add` sub-command:
|
||||
|
||||
```bash
|
||||
argocd gpg add --from <path-to-key>
|
||||
```
|
||||
|
||||
The key to be imported can be either in binary or ASCII-armored format.
|
||||
|
||||
##### Removing a key from the configuration
|
||||
|
||||
To remove a previously configured key from the configuration, use the
|
||||
`argocd gpg rm` sub-command:
|
||||
|
||||
```bash
|
||||
argocd gpg rm <key-id>
|
||||
```
|
||||
|
||||
#### Manage public keys using the Web UI
|
||||
|
||||
Basic key management functionality for listing, importing and removing GnuPG
|
||||
public keys is implemented in the Web UI. You can find the configuration
|
||||
module from the **Settings** page in the **GnuPG keys** module.
|
||||
|
||||
Please note that when you configure keys using the Web UI, the key must be
|
||||
imported in ASCII armored format for now.
|
||||
|
||||
#### Manage public keys in declarative setup
|
||||
|
||||
ArgoCD stores public keys internally in the `argocd-gpg-keys-cm` ConfigMap
|
||||
resource, with the public GnuPG key's ID as its name and the ASCII armored
|
||||
key data as string value, i.e. the entry for the GitHub's web-flow signing
|
||||
key would look like follows:
|
||||
|
||||
```yaml
|
||||
4AEE18F83AFDEB23: |
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBFmUaEEBCACzXTDt6ZnyaVtueZASBzgnAmK13q9Urgch+sKYeIhdymjuMQta
|
||||
x15OklctmrZtqre5kwPUosG3/B2/ikuPYElcHgGPL4uL5Em6S5C/oozfkYzhwRrT
|
||||
SQzvYjsE4I34To4UdE9KA97wrQjGoz2Bx72WDLyWwctD3DKQtYeHXswXXtXwKfjQ
|
||||
7Fy4+Bf5IPh76dA8NJ6UtjjLIDlKqdxLW4atHe6xWFaJ+XdLUtsAroZcXBeWDCPa
|
||||
buXCDscJcLJRKZVc62gOZXXtPfoHqvUPp3nuLA4YjH9bphbrMWMf810Wxz9JTd3v
|
||||
yWgGqNY0zbBqeZoGv+TuExlRHT8ASGFS9SVDABEBAAG0NUdpdEh1YiAod2ViLWZs
|
||||
b3cgY29tbWl0IHNpZ25pbmcpIDxub3JlcGx5QGdpdGh1Yi5jb20+iQEiBBMBCAAW
|
||||
BQJZlGhBCRBK7hj4Ov3rIwIbAwIZAQAAmQEH/iATWFmi2oxlBh3wAsySNCNV4IPf
|
||||
DDMeh6j80WT7cgoX7V7xqJOxrfrqPEthQ3hgHIm7b5MPQlUr2q+UPL22t/I+ESF6
|
||||
9b0QWLFSMJbMSk+BXkvSjH9q8jAO0986/pShPV5DU2sMxnx4LfLfHNhTzjXKokws
|
||||
+8ptJ8uhMNIDXfXuzkZHIxoXk3rNcjDN5c5X+sK8UBRH092BIJWCOfaQt7v7wig5
|
||||
4Ra28pM9GbHKXVNxmdLpCFyzvyMuCmINYYADsC848QQFFwnd4EQnupo6QvhEVx1O
|
||||
j7wDwvuH5dCrLuLwtwXaQh0onG4583p0LGms2Mf5F+Ick6o/4peOlBoZz48=
|
||||
=Bvzs
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
```
|
||||
|
||||
## Policies for GnuPG signature verification
|
||||
|
||||
The GnuPG commit signature verification is configured through one or multiple Git `gpg` policies.
|
||||
|
||||
The policies are configured as illustrated:
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: AppProject
|
||||
spec:
|
||||
sourceIntegrity:
|
||||
git:
|
||||
policies:
|
||||
- repos:
|
||||
- url: "https://github.com/my-group/*"
|
||||
- url: "!https://github.com/my-group/ignored.git"
|
||||
gpg:
|
||||
mode: "none|head|strict"
|
||||
keys:
|
||||
- "D56C4FCA57A46444"
|
||||
```
|
||||
|
||||
The `repos ` key contains a list of glob-style patterns matched against the URL of the source to verify.
|
||||
Given strategy will be used when matched some of the positive globs, while not matched by any of the negative ones (starting with `!`).
|
||||
|
||||
Only one policy is applied per source repository, and sources not matched by any policy will not have its integrity verified.
|
||||
|
||||
Note that a multi-source application can have each of its source repositories validated against a different policy.
|
||||
|
||||
### The `gpg` verification policy
|
||||
|
||||
The Git commit signature verification is an alternative to calling `git verify-commit`/`git verify-tag` with configured keyring and making sure the key ID used for the signatures is among the configured Key IDs in the source integrity policy.
|
||||
If the target revision points to a commit or tags that do not satisfy those criteria, it will not be synced.
|
||||
|
||||
The `keys` key lists the set of key IDs to trust for signed commits.
|
||||
If a commit in the repository is signed by an ID not specified in the list of trusted signers, the verification will fail.
|
||||
|
||||
The `mode` defines how thorough the GnuPG verification is:
|
||||
|
||||
##### Verification mode `none`
|
||||
|
||||
Verification is not performed for this strategy, and no following strategies are tried.
|
||||
|
||||
Note this accepts unsigned commits as well as commits with a signature that is invalid in some sense (expired, unverifiable, etc.).
|
||||
|
||||
##### Verification mode `head`
|
||||
|
||||
Verify only the commit/tag pointed to by the target revision of the source.
|
||||
If the revision is an annotated tag, it is the tag's signature that is verified, not the commit's signature (i.e. the tag itself must be signed using `git tag -s`).
|
||||
Otherwise, if target revision is a branch name, reference name (such as `HEAD`), or a commit SHA Argo CD verifies the commit's GnuPG signature.
|
||||
|
||||
##### Verification mode `strict`
|
||||
|
||||
Verify target revision and all its ancestors.
|
||||
This makes sure there is no unsigned change in the history as well.
|
||||
If the revision is an annotated tag, the tag's signature is verified together with the commit history, including the commit it points to.
|
||||
|
||||
There are situations where verifying the entire history is not practical - typically in case the history contains unsigned commits, or commits signed with keys that are no longer trusted.
|
||||
This happens when GnuPG verification is introduced later to the git repository, or when formerly accepted keys get removed, revoked, or rotated.
|
||||
While this can be addressed by re-signing with git rebase, there is a better way that does not require rewriting the Git history.
|
||||
|
||||
###### Commit seal-signing with `strict` mode
|
||||
|
||||
A sealing commit is a GnuPG signed commit that works as a "seal of approval" attesting that all its ancestor commits were either signed by a trusted key, or reviewed and trusted by the author of the sealing commit.
|
||||
Argo CD verifying GnuPG signatures would then progress only as far back in the history as the most recent "seal" commits in each individual ancestral branch.
|
||||
|
||||
In practice, a committer first *reviews* all commits that are not signed or signed with untrusted keys from the previous "seal commit" and creates a new, possibly empty commit with a custom Git trailer in its message.
|
||||
Such commits can have the following organization-level semantics:
|
||||
|
||||
- "From now on, we are going to GnuPG sign all commits in this repository. There is no point in verifying the unsigned ones from before."
|
||||
- "I merge these changes from untrusted external contributor, and I approve of them."
|
||||
- "I am removing the GnuPG key of Bob. All his previous commits are trusted, but no new ones will be. Happy retirement, Bob!"
|
||||
- "I am replacing my old key with a new one. Trust my commits signed with the old key before this seal commit, but only trust my new key after this seal commit."
|
||||
|
||||
To create a seal commit, run `git commit --signoff --gpg-sign --trailer="Argocd-gpg-seal: <justification>"` and push to branch pulled by Argo CD.
|
||||
Using seal commits is preferable to rewriting git history as it eliminates the room for eventual rebasing mistakes that would jeopardize either source integrity or correctness of the repository data.
|
||||
|
||||
## Upgrade to Source Integrity Verification
|
||||
|
||||
To migrate from the legacy declaration to the new source verification policies, remove `.spec.signatureKeys` and then define desired policies in `.spec.sourceIntegrity.git.policies`.
|
||||
|
||||
To achieve the legacy Argo CD verification behavior in a project, use the following config:
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: AppProject
|
||||
spec:
|
||||
sourceIntegrity:
|
||||
git:
|
||||
policies:
|
||||
- repos:
|
||||
- url: "*" # For any repository in the project
|
||||
gpg:
|
||||
mode: "head" # Verify only the HEAD of the target revision
|
||||
keys:
|
||||
- "..." # Keys from .spec.signatureKeys
|
||||
```
|
||||
|
||||
When `.spec.sourceIntegrity` is not defined but `.spec.signatureKeys` is, Argo CD will do similar conversion behind the scenes.
|
||||
Though it is advised to perform the migration as source integrity config allows for greater flexibility, and `.spec.signatureKeys` will be a subject of removal in future releases.
|
||||
|
||||
## Downgrade from Source Integrity Verification
|
||||
|
||||
At downgrade time, reintroduce `.spec.signatureKeys` in any AppProject, populate it with all the keys from `.spec.sourceIntegrity.git.policies`, and then delete the `.spec.sourceIntegrity` section.
|
||||
Mind the legacy functionality lacks many of the new features—it will be all "head" mode for all project repositories.
|
||||
|
||||
As an alternative to downgrade, consult the troubleshooting section here.
|
||||
|
||||
## Legacy signature key management (DEPRECATED)
|
||||
|
||||
The project-wide signature keys can be managed through UI and CLI.
|
||||
Note they are being replaced by the source integrity policies, so users are advised to migrate away from these.
|
||||
|
||||
### Configuring using the CLI (DEPRECATED)
|
||||
|
||||
#### Adding a key ID to the list of allowed keys
|
||||
|
||||
To add a key ID to the list of allowed GnuPG keys for a project, you can use
|
||||
the `argocd proj add-signature-key` command, i.e. the following command would
|
||||
add the key ID `4AEE18F83AFDEB23` to the project named `myproj`:
|
||||
|
||||
```bash
|
||||
# DEPRECATED
|
||||
argocd proj add-signature-key myproj 4AEE18F83AFDEB23
|
||||
```
|
||||
|
||||
#### Removing a key ID from the list of allowed keys
|
||||
|
||||
Similarly, you can remove a key ID from the list of allowed GnuPG keys for a
|
||||
project using the `argocd proj remove-signature-key` command, i.e. to remove
|
||||
the key added above from project `myproj`, use the command:
|
||||
|
||||
```bash
|
||||
# DEPRECATED
|
||||
argocd proj remove-signature-key myproj 4AEE18F83AFDEB23
|
||||
```
|
||||
|
||||
#### Showing allowed key IDs for a project
|
||||
|
||||
To see which key IDs are allowed for a given project, you can inspect the
|
||||
output of the `argocd proj get` command, i.e. for a project named `gpg`:
|
||||
|
||||
```bash
|
||||
# DEPRECATED
|
||||
$ argocd proj get gpg
|
||||
Name: gpg
|
||||
Description: GnuPG verification
|
||||
Destinations: *,*
|
||||
Repositories: *
|
||||
Allowed Cluster Resources: */*
|
||||
Denied Namespaced Resources: <none>
|
||||
Signature keys: 4AEE18F83AFDEB23, 07E34825A909B250
|
||||
Orphaned Resources: disabled
|
||||
```
|
||||
|
||||
#### Override list of key IDs
|
||||
|
||||
You can also explicitly set the currently allowed keys with one or more new keys
|
||||
using the `argocd proj set` command in combination with the `--signature-keys`
|
||||
flag, which you can use to specify a comma separated list of allowed key IDs:
|
||||
|
||||
```bash
|
||||
# DEPRECATED
|
||||
argocd proj set myproj --signature-keys 4AEE18F83AFDEB23,07E34825A909B250
|
||||
```
|
||||
|
||||
The `--signature-keys` flag can also be used on project creation, i.e. the
|
||||
`argocd proj create` command.
|
||||
|
||||
### Configure using the Web UI (DEPRECATED)
|
||||
|
||||
You can configure the GnuPG key IDs required for signature verification using
|
||||
the web UI, in the Project configuration. Navigate to the **Settings** page
|
||||
and select the **Projects** module, then click on the project you want to
|
||||
configure.
|
||||
|
||||
From the project's details page, click **Edit** and find the
|
||||
**Required signature keys** section, where you can add or remove the key IDs
|
||||
for signature verification. After you have modified your project, click
|
||||
**Update** to save the changes.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Disabling the feature
|
||||
|
||||
The GnuPG feature can be completely disabled if desired. In order to disable it,
|
||||
set the environment variable `ARGOCD_GPG_ENABLED` to `false` for the pod
|
||||
templates of the `argocd-server`, `argocd-repo-server`, `argocd-application-controller`
|
||||
and `argocd-applicationset-controller` deployment manifests.
|
||||
|
||||
After the pods have been restarted, the GnuPG feature is disabled.
|
||||
|
||||
### Inspecting GnuPG key ring
|
||||
|
||||
The GnuPG key ring used for signature verification is maintained within the
|
||||
pods of `argocd-repo-server`. The keys in the keyring are synchronized to the
|
||||
configuration stored in the `argocd-gpg-keys-cm` ConfigMap resource, which is
|
||||
volume-mounted to the `argocd-repo-server` pods.
|
||||
|
||||
> [!NOTE]
|
||||
> The GnuPG key ring in the pods is transient and gets recreated from the
|
||||
> configuration on each restart of the pods. You should never add or remove
|
||||
> keys manually to the key ring in the pod, because your changes will be lost. Also,
|
||||
> any of the private keys found in the key ring are transient and will be
|
||||
> regenerated upon each restart. The private key is only used to build the
|
||||
> trust DB for the running pod.
|
||||
|
||||
To check whether the keys are actually in sync, you can `kubectl exec` into the
|
||||
repository server's pods and inspect the key ring, which is located at path
|
||||
`/app/config/gpg/keys`
|
||||
|
||||
```bash
|
||||
$ kubectl exec -it argocd-repo-server-7d6bdfdf6d-hzqkg bash
|
||||
argocd@argocd-repo-server-7d6bdfdf6d-hzqkg:~$ GNUPGHOME=/app/config/gpg/keys gpg --list-keys
|
||||
/app/config/gpg/keys/pubring.kbx
|
||||
--------------------------------
|
||||
pub rsa2048 2020-06-15 [SC] [expires: 2020-12-12]
|
||||
D48F075D818A813C436914BC9324F0D2144753B1
|
||||
uid [ultimate] Anon Ymous (ArgoCD key signing key) <noreply@argoproj.io>
|
||||
|
||||
pub rsa2048 2017-08-16 [SC]
|
||||
5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB23
|
||||
uid [ultimate] GitHub (web-flow commit signing) <noreply@github.com>
|
||||
|
||||
argocd@argocd-repo-server-7d6bdfdf6d-hzqkg:~$
|
||||
```
|
||||
|
||||
If the key ring stays out of sync with your configuration after you have added
|
||||
or removed keys for a longer period of time, you might want to restart your
|
||||
`argocd-repo-server` pods. If such a problem persists, please consider raising
|
||||
a bug report.
|
||||
29
docs/user-guide/source-integrity.md
Normal file
29
docs/user-guide/source-integrity.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Overview
|
||||
|
||||
Argo CD permits declaring criteria for application sources integrity that, when not met, will prevent an application from syncing with a `ResourceComparison` error.
|
||||
This is useful to verify the sources have not been tampered with by an unauthorized contributor.
|
||||
|
||||
Each Application Project can have its criteria configured in `AppProject`'s `.spec.sourceIntegrity`.
|
||||
The criteria distinguish a type of verification they perform, and to which sources they apply.
|
||||
|
||||
Each application can be a subject of multiple checks, and the sync will be enabled only when all criteria are met.
|
||||
|
||||
> [!NOTE]
|
||||
> Source Integrity Verification is only configured through `AppProject` manifests at this point. CLI and UI are not supported.
|
||||
|
||||
> [!NOTE]
|
||||
> Signature verification is not supported for the Application Sets populated by the git generator when they have the `project` field templated.
|
||||
|
||||
> [!WARNING]
|
||||
> If source integrity is enforced, you will not be able to sync from local sources (i.e. `argocd app sync --local`) anymore.
|
||||
|
||||
## Supported methods
|
||||
|
||||
- [Git GnuPG verification](./source-integrity-git-gpg.md) verifies that Git commits are GnuPG Signed. This is a modern method of the commit signature verification originally configured in `AppProjects`'s `signatureKeys`.
|
||||
|
||||
## Multi-source applications
|
||||
|
||||
Each individual application source can be a subject of a different set of source integrity criteria, if desirable.
|
||||
This is necessary if the sources are of a different type, such as Git and Helm.
|
||||
But even different repositories of the same type can utilize different methods of verification, or their different configurations.
|
||||
This is useful when an application combines sources maintained by different groups of people, or according to different contribution (and signing) guidelines.
|
||||
2
go.mod
2
go.mod
|
|
@ -115,7 +115,7 @@ require (
|
|||
k8s.io/klog/v2 v2.140.0
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
|
||||
k8s.io/kubectl v0.34.0
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
|
||||
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427
|
||||
oras.land/oras-go/v2 v2.6.0
|
||||
sigs.k8s.io/controller-runtime v0.21.0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
# DEPRECATED: To be removed in the next major version when Signature verification is replaced with Source Integrity.
|
||||
|
||||
# Wrapper script to perform GPG signature validation on git commit SHAs and
|
||||
# annotated tags.
|
||||
#
|
||||
|
|
|
|||
49
manifests/core-install-with-hydrator.yaml
generated
49
manifests/core-install-with-hydrator.yaml
generated
|
|
@ -30467,6 +30467,55 @@ spec:
|
|||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceIntegrity:
|
||||
description: |-
|
||||
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
|
||||
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
|
||||
properties:
|
||||
git:
|
||||
description: Git - policies for git source verification
|
||||
properties:
|
||||
policies:
|
||||
items:
|
||||
properties:
|
||||
gpg:
|
||||
description: Verify GPG commit/tag signatures
|
||||
properties:
|
||||
keys:
|
||||
description: List of key IDs to trust. The keys
|
||||
need to be in the repository server keyring.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
mode:
|
||||
type: string
|
||||
required:
|
||||
- keys
|
||||
- mode
|
||||
type: object
|
||||
repos:
|
||||
description: List of repository criteria restricting
|
||||
repositories the policy will apply to
|
||||
items:
|
||||
properties:
|
||||
url:
|
||||
description: URL specifier, glob.
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- gpg
|
||||
- repos
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- policies
|
||||
type: object
|
||||
required:
|
||||
- git
|
||||
type: object
|
||||
sourceNamespaces:
|
||||
description: SourceNamespaces defines the namespaces application resources
|
||||
are allowed to be created in
|
||||
|
|
|
|||
49
manifests/core-install.yaml
generated
49
manifests/core-install.yaml
generated
|
|
@ -30467,6 +30467,55 @@ spec:
|
|||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceIntegrity:
|
||||
description: |-
|
||||
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
|
||||
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
|
||||
properties:
|
||||
git:
|
||||
description: Git - policies for git source verification
|
||||
properties:
|
||||
policies:
|
||||
items:
|
||||
properties:
|
||||
gpg:
|
||||
description: Verify GPG commit/tag signatures
|
||||
properties:
|
||||
keys:
|
||||
description: List of key IDs to trust. The keys
|
||||
need to be in the repository server keyring.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
mode:
|
||||
type: string
|
||||
required:
|
||||
- keys
|
||||
- mode
|
||||
type: object
|
||||
repos:
|
||||
description: List of repository criteria restricting
|
||||
repositories the policy will apply to
|
||||
items:
|
||||
properties:
|
||||
url:
|
||||
description: URL specifier, glob.
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- gpg
|
||||
- repos
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- policies
|
||||
type: object
|
||||
required:
|
||||
- git
|
||||
type: object
|
||||
sourceNamespaces:
|
||||
description: SourceNamespaces defines the namespaces application resources
|
||||
are allowed to be created in
|
||||
|
|
|
|||
49
manifests/crds/appproject-crd.yaml
generated
49
manifests/crds/appproject-crd.yaml
generated
|
|
@ -268,6 +268,55 @@ spec:
|
|||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceIntegrity:
|
||||
description: |-
|
||||
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
|
||||
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
|
||||
properties:
|
||||
git:
|
||||
description: Git - policies for git source verification
|
||||
properties:
|
||||
policies:
|
||||
items:
|
||||
properties:
|
||||
gpg:
|
||||
description: Verify GPG commit/tag signatures
|
||||
properties:
|
||||
keys:
|
||||
description: List of key IDs to trust. The keys
|
||||
need to be in the repository server keyring.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
mode:
|
||||
type: string
|
||||
required:
|
||||
- keys
|
||||
- mode
|
||||
type: object
|
||||
repos:
|
||||
description: List of repository criteria restricting
|
||||
repositories the policy will apply to
|
||||
items:
|
||||
properties:
|
||||
url:
|
||||
description: URL specifier, glob.
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- gpg
|
||||
- repos
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- policies
|
||||
type: object
|
||||
required:
|
||||
- git
|
||||
type: object
|
||||
sourceNamespaces:
|
||||
description: SourceNamespaces defines the namespaces application resources
|
||||
are allowed to be created in
|
||||
|
|
|
|||
49
manifests/ha/install-with-hydrator.yaml
generated
49
manifests/ha/install-with-hydrator.yaml
generated
|
|
@ -30467,6 +30467,55 @@ spec:
|
|||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceIntegrity:
|
||||
description: |-
|
||||
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
|
||||
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
|
||||
properties:
|
||||
git:
|
||||
description: Git - policies for git source verification
|
||||
properties:
|
||||
policies:
|
||||
items:
|
||||
properties:
|
||||
gpg:
|
||||
description: Verify GPG commit/tag signatures
|
||||
properties:
|
||||
keys:
|
||||
description: List of key IDs to trust. The keys
|
||||
need to be in the repository server keyring.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
mode:
|
||||
type: string
|
||||
required:
|
||||
- keys
|
||||
- mode
|
||||
type: object
|
||||
repos:
|
||||
description: List of repository criteria restricting
|
||||
repositories the policy will apply to
|
||||
items:
|
||||
properties:
|
||||
url:
|
||||
description: URL specifier, glob.
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- gpg
|
||||
- repos
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- policies
|
||||
type: object
|
||||
required:
|
||||
- git
|
||||
type: object
|
||||
sourceNamespaces:
|
||||
description: SourceNamespaces defines the namespaces application resources
|
||||
are allowed to be created in
|
||||
|
|
|
|||
49
manifests/ha/install.yaml
generated
49
manifests/ha/install.yaml
generated
|
|
@ -30467,6 +30467,55 @@ spec:
|
|||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceIntegrity:
|
||||
description: |-
|
||||
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
|
||||
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
|
||||
properties:
|
||||
git:
|
||||
description: Git - policies for git source verification
|
||||
properties:
|
||||
policies:
|
||||
items:
|
||||
properties:
|
||||
gpg:
|
||||
description: Verify GPG commit/tag signatures
|
||||
properties:
|
||||
keys:
|
||||
description: List of key IDs to trust. The keys
|
||||
need to be in the repository server keyring.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
mode:
|
||||
type: string
|
||||
required:
|
||||
- keys
|
||||
- mode
|
||||
type: object
|
||||
repos:
|
||||
description: List of repository criteria restricting
|
||||
repositories the policy will apply to
|
||||
items:
|
||||
properties:
|
||||
url:
|
||||
description: URL specifier, glob.
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- gpg
|
||||
- repos
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- policies
|
||||
type: object
|
||||
required:
|
||||
- git
|
||||
type: object
|
||||
sourceNamespaces:
|
||||
description: SourceNamespaces defines the namespaces application resources
|
||||
are allowed to be created in
|
||||
|
|
|
|||
49
manifests/install-with-hydrator.yaml
generated
49
manifests/install-with-hydrator.yaml
generated
|
|
@ -30467,6 +30467,55 @@ spec:
|
|||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceIntegrity:
|
||||
description: |-
|
||||
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
|
||||
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
|
||||
properties:
|
||||
git:
|
||||
description: Git - policies for git source verification
|
||||
properties:
|
||||
policies:
|
||||
items:
|
||||
properties:
|
||||
gpg:
|
||||
description: Verify GPG commit/tag signatures
|
||||
properties:
|
||||
keys:
|
||||
description: List of key IDs to trust. The keys
|
||||
need to be in the repository server keyring.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
mode:
|
||||
type: string
|
||||
required:
|
||||
- keys
|
||||
- mode
|
||||
type: object
|
||||
repos:
|
||||
description: List of repository criteria restricting
|
||||
repositories the policy will apply to
|
||||
items:
|
||||
properties:
|
||||
url:
|
||||
description: URL specifier, glob.
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- gpg
|
||||
- repos
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- policies
|
||||
type: object
|
||||
required:
|
||||
- git
|
||||
type: object
|
||||
sourceNamespaces:
|
||||
description: SourceNamespaces defines the namespaces application resources
|
||||
are allowed to be created in
|
||||
|
|
|
|||
49
manifests/install.yaml
generated
49
manifests/install.yaml
generated
|
|
@ -30467,6 +30467,55 @@ spec:
|
|||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceIntegrity:
|
||||
description: |-
|
||||
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
|
||||
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
|
||||
properties:
|
||||
git:
|
||||
description: Git - policies for git source verification
|
||||
properties:
|
||||
policies:
|
||||
items:
|
||||
properties:
|
||||
gpg:
|
||||
description: Verify GPG commit/tag signatures
|
||||
properties:
|
||||
keys:
|
||||
description: List of key IDs to trust. The keys
|
||||
need to be in the repository server keyring.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
mode:
|
||||
type: string
|
||||
required:
|
||||
- keys
|
||||
- mode
|
||||
type: object
|
||||
repos:
|
||||
description: List of repository criteria restricting
|
||||
repositories the policy will apply to
|
||||
items:
|
||||
properties:
|
||||
url:
|
||||
description: URL specifier, glob.
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- gpg
|
||||
- repos
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- policies
|
||||
type: object
|
||||
required:
|
||||
- git
|
||||
type: object
|
||||
sourceNamespaces:
|
||||
description: SourceNamespaces defines the namespaces application resources
|
||||
are allowed to be created in
|
||||
|
|
|
|||
|
|
@ -180,7 +180,9 @@ nav:
|
|||
- user-guide/private-repositories.md
|
||||
- user-guide/plugins.md
|
||||
- user-guide/multiple_sources.md
|
||||
- GnuPG verification: user-guide/gpg-verification.md
|
||||
- Source Integrity Verification:
|
||||
- user-guide/source-integrity.md
|
||||
- Git GnuPG verification: user-guide/source-integrity-git-gpg.md
|
||||
- user-guide/auto_sync.md
|
||||
- Diffing:
|
||||
- Diff Strategies: user-guide/diff-strategies.md
|
||||
|
|
|
|||
3093
pkg/apis/application/v1alpha1/generated.pb.go
generated
3093
pkg/apis/application/v1alpha1/generated.pb.go
generated
File diff suppressed because it is too large
Load diff
|
|
@ -111,6 +111,10 @@ message AppProjectSpec {
|
|||
|
||||
// DestinationServiceAccounts holds information about the service accounts to be impersonated for the application sync operation for each destination.
|
||||
repeated ApplicationDestinationServiceAccount destinationServiceAccounts = 14;
|
||||
|
||||
// SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
|
||||
// Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
|
||||
optional SourceIntegrity sourceIntegrity = 15;
|
||||
}
|
||||
|
||||
// AppProjectStatus contains status information for AppProject CRs
|
||||
|
|
@ -2362,10 +2366,14 @@ message RevisionMetadata {
|
|||
optional string message = 4;
|
||||
|
||||
// SignatureInfo contains a hint on the signer if the revision was signed with GPG, and signature verification is enabled.
|
||||
//
|
||||
// Deprecated: Use SourceIntegrityResult for more detailed information. SignatureInfo will be removed with the next major version.
|
||||
optional string signatureInfo = 5;
|
||||
|
||||
// References contains references to information that's related to this commit in some way.
|
||||
repeated RevisionReference references = 6;
|
||||
|
||||
optional SourceIntegrityCheckResult sourceIntegrityResult = 7;
|
||||
}
|
||||
|
||||
// RevisionReference contains a reference to a some information that is related in some way to another commit. For now,
|
||||
|
|
@ -2613,6 +2621,60 @@ message SourceHydratorStatus {
|
|||
optional HydrateOperation currentOperation = 2;
|
||||
}
|
||||
|
||||
message SourceIntegrity {
|
||||
// Git - policies for git source verification
|
||||
optional SourceIntegrityGit git = 1;
|
||||
}
|
||||
|
||||
// SourceIntegrityCheckResult represents a conclusion of the SourceIntegrity evaluation.
|
||||
// Each check performed on a source(es), holds a check item representing all checks performed.
|
||||
message SourceIntegrityCheckResult {
|
||||
// Checks holds a list of checks performed, with their eventual problems. If a check is not specified here,
|
||||
// it means it was not performed.
|
||||
repeated SourceIntegrityCheckResultItem checks = 1;
|
||||
}
|
||||
|
||||
message SourceIntegrityCheckResultItem {
|
||||
// Name of the check that is human-understandable pointing out to the kind of verification performed.
|
||||
optional string name = 1;
|
||||
|
||||
// Problems is a list of messages explaining why the check failed. Empty list means the check has succeeded.
|
||||
repeated string problems = 2;
|
||||
}
|
||||
|
||||
message SourceIntegrityGit {
|
||||
repeated SourceIntegrityGitPolicy policies = 1;
|
||||
}
|
||||
|
||||
message SourceIntegrityGitPolicy {
|
||||
// List of repository criteria restricting repositories the policy will apply to
|
||||
repeated SourceIntegrityGitPolicyRepo repos = 1;
|
||||
|
||||
// Verify GPG commit/tag signatures
|
||||
optional SourceIntegrityGitPolicyGPG gpg = 2;
|
||||
}
|
||||
|
||||
// SourceIntegrityGitPolicyGPG verifies that the commit(s) are both correctly signed by a key in the repo-server keyring,
|
||||
// and that they are signed by one of the key listed in Keys.
|
||||
//
|
||||
// This policy can be deactivated through the ARGOCD_GPG_ENABLED environment variable.
|
||||
//
|
||||
// Note the listing of problematic commits/signatures reported when "strict" mode validation fails may not be complete.
|
||||
// This means that a user that has addressed all problems reported by source integrity check can run into
|
||||
// further problematic signatures on a subsequent attempt. That happens namely when history contains seal commits signed
|
||||
// with gpg keys that are in the keyring, but not listed in Keys.
|
||||
message SourceIntegrityGitPolicyGPG {
|
||||
optional string mode = 1;
|
||||
|
||||
// List of key IDs to trust. The keys need to be in the repository server keyring.
|
||||
repeated string keys = 3;
|
||||
}
|
||||
|
||||
message SourceIntegrityGitPolicyRepo {
|
||||
// URL specifier, glob.
|
||||
optional string url = 1;
|
||||
}
|
||||
|
||||
// SuccessfulHydrateOperation contains information about the most recent successful hydrate operation
|
||||
message SuccessfulHydrateOperation {
|
||||
// DrySHA holds the resolved revision (sha) of the dry source as of the most recent reconciliation
|
||||
|
|
|
|||
119
pkg/apis/application/v1alpha1/source_integrity.go
Normal file
119
pkg/apis/application/v1alpha1/source_integrity.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package v1alpha1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type SourceIntegrity struct {
|
||||
// Git - policies for git source verification
|
||||
Git *SourceIntegrityGit `json:"git" protobuf:"bytes,1,name=git"` // A mandatory field until there are alternatives
|
||||
}
|
||||
|
||||
type SourceIntegrityGit struct {
|
||||
Policies []*SourceIntegrityGitPolicy `json:"policies" protobuf:"bytes,1,name=policies"`
|
||||
}
|
||||
|
||||
type SourceIntegrityGitPolicy struct {
|
||||
// List of repository criteria restricting repositories the policy will apply to
|
||||
Repos []SourceIntegrityGitPolicyRepo `json:"repos" protobuf:"bytes,1,name=repos"`
|
||||
// Verify GPG commit/tag signatures
|
||||
GPG *SourceIntegrityGitPolicyGPG `json:"gpg" protobuf:"bytes,2,name=gpg"` // A mandatory field until there are alternatives
|
||||
}
|
||||
|
||||
type SourceIntegrityGitPolicyRepo struct {
|
||||
// URL specifier, glob.
|
||||
URL string `json:"url" protobuf:"bytes,1,name=url"`
|
||||
}
|
||||
|
||||
type SourceIntegrityGitPolicyGPGMode string
|
||||
|
||||
var (
|
||||
// SourceIntegrityGitPolicyGPGModeNone performs no verification at all. This is useful for troubleshooting.
|
||||
SourceIntegrityGitPolicyGPGModeNone SourceIntegrityGitPolicyGPGMode = "none"
|
||||
// SourceIntegrityGitPolicyGPGModeHead verifies the current target revision, an annotated tag, or a commit.
|
||||
SourceIntegrityGitPolicyGPGModeHead SourceIntegrityGitPolicyGPGMode = "head"
|
||||
// SourceIntegrityGitPolicyGPGModeStrict verifies all ancestry of target revision all the way to git init or seal commits.
|
||||
// If pointing to an annotated tag, it verifies both the tag signature and the commit history.
|
||||
SourceIntegrityGitPolicyGPGModeStrict SourceIntegrityGitPolicyGPGMode = "strict"
|
||||
)
|
||||
|
||||
// SourceIntegrityGitPolicyGPG verifies that the commit(s) are both correctly signed by a key in the repo-server keyring,
|
||||
// and that they are signed by one of the key listed in Keys.
|
||||
//
|
||||
// This policy can be deactivated through the ARGOCD_GPG_ENABLED environment variable.
|
||||
//
|
||||
// Note the listing of problematic commits/signatures reported when "strict" mode validation fails may not be complete.
|
||||
// This means that a user that has addressed all problems reported by source integrity check can run into
|
||||
// further problematic signatures on a subsequent attempt. That happens namely when history contains seal commits signed
|
||||
// with gpg keys that are in the keyring, but not listed in Keys.
|
||||
type SourceIntegrityGitPolicyGPG struct {
|
||||
Mode SourceIntegrityGitPolicyGPGMode `json:"mode" protobuf:"bytes,1,name=mode"`
|
||||
// List of key IDs to trust. The keys need to be in the repository server keyring.
|
||||
Keys []string `json:"keys" protobuf:"bytes,3,name=keys"`
|
||||
}
|
||||
|
||||
// SourceIntegrityCheckResult represents a conclusion of the SourceIntegrity evaluation.
|
||||
// Each check performed on a source(es), holds a check item representing all checks performed.
|
||||
type SourceIntegrityCheckResult struct {
|
||||
// Checks holds a list of checks performed, with their eventual problems. If a check is not specified here,
|
||||
// it means it was not performed.
|
||||
Checks []SourceIntegrityCheckResultItem `protobuf:"bytes,1,opt,name=checks"`
|
||||
}
|
||||
|
||||
type SourceIntegrityCheckResultItem struct {
|
||||
// Name of the check that is human-understandable pointing out to the kind of verification performed.
|
||||
Name string `protobuf:"bytes,1,name=name"`
|
||||
// Problems is a list of messages explaining why the check failed. Empty list means the check has succeeded.
|
||||
Problems []string `protobuf:"bytes,2,name=problems"`
|
||||
}
|
||||
|
||||
func (r *SourceIntegrityCheckResult) PassedChecks() (names []string) {
|
||||
names = []string{}
|
||||
for _, item := range r.Checks {
|
||||
if len(item.Problems) == 0 {
|
||||
names = append(names, item.Name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (r *SourceIntegrityCheckResult) AsError() error {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
var errs []error
|
||||
for _, check := range r.Checks {
|
||||
for _, p := range check.Problems {
|
||||
errs = append(errs, fmt.Errorf("%s: %s", check.Name, p))
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValid reports if some of the performed checks had any problem
|
||||
func (r *SourceIntegrityCheckResult) IsValid() bool {
|
||||
for _, item := range r.Checks {
|
||||
if len(item.Problems) > 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// InjectSourceName updates the names of the checks with a new prefix. This is to distinguish results reported when
|
||||
// checking multiple sources.
|
||||
func (r *SourceIntegrityCheckResult) InjectSourceName(sourceName string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
for chi, check := range r.Checks {
|
||||
for pi, problem := range check.Problems {
|
||||
r.Checks[chi].Problems[pi] = fmt.Sprintf("%s: %s", sourceName, problem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1693,9 +1693,12 @@ type RevisionMetadata struct {
|
|||
// Message contains the message associated with the revision, most likely the commit message.
|
||||
Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"`
|
||||
// SignatureInfo contains a hint on the signer if the revision was signed with GPG, and signature verification is enabled.
|
||||
//
|
||||
// Deprecated: Use SourceIntegrityResult for more detailed information. SignatureInfo will be removed with the next major version.
|
||||
SignatureInfo string `json:"signatureInfo,omitempty" protobuf:"bytes,5,opt,name=signatureInfo"`
|
||||
// References contains references to information that's related to this commit in some way.
|
||||
References []RevisionReference `json:"references,omitempty" protobuf:"bytes,6,opt,name=references"`
|
||||
References []RevisionReference `json:"references,omitempty" protobuf:"bytes,6,opt,name=references"`
|
||||
SourceIntegrityResult *SourceIntegrityCheckResult `json:"sourceIntegrityResult,omitempty" protobuf:"bytes,7,opt,name=sourceIntegrityResult"`
|
||||
}
|
||||
|
||||
// OCIMetadata contains metadata for a specific revision in an OCI repository
|
||||
|
|
@ -2801,6 +2804,50 @@ type AppProjectSpec struct {
|
|||
PermitOnlyProjectScopedClusters bool `json:"permitOnlyProjectScopedClusters,omitempty" protobuf:"bytes,13,opt,name=permitOnlyProjectScopedClusters"`
|
||||
// DestinationServiceAccounts holds information about the service accounts to be impersonated for the application sync operation for each destination.
|
||||
DestinationServiceAccounts []ApplicationDestinationServiceAccount `json:"destinationServiceAccounts,omitempty" protobuf:"bytes,14,name=destinationServiceAccounts"`
|
||||
// SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
|
||||
// Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
|
||||
SourceIntegrity *SourceIntegrity `json:"sourceIntegrity,omitempty" protobuf:"bytes,15,name=sourceIntegrity"`
|
||||
}
|
||||
|
||||
// EffectiveSourceIntegrity incorporates the legacy SignatureKeys into SourceIntegrity, if possible
|
||||
// SignatureKeys are added as a Git GPG policy for repos specified with `*`. If such a policy exists, the SignatureKeys
|
||||
// are ignored with warning.
|
||||
func (proj *AppProject) EffectiveSourceIntegrity() *SourceIntegrity {
|
||||
var legacyKeys []string
|
||||
for _, k := range proj.Spec.SignatureKeys {
|
||||
legacyKeys = append(legacyKeys, k.KeyID)
|
||||
}
|
||||
|
||||
if len(legacyKeys) == 0 {
|
||||
// Already using the modern version
|
||||
return proj.Spec.SourceIntegrity
|
||||
}
|
||||
|
||||
migratedGit := &SourceIntegrityGit{
|
||||
Policies: []*SourceIntegrityGitPolicy{{
|
||||
Repos: []SourceIntegrityGitPolicyRepo{{URL: "*"}},
|
||||
GPG: &SourceIntegrityGitPolicyGPG{
|
||||
Mode: SourceIntegrityGitPolicyGPGModeHead,
|
||||
Keys: legacyKeys,
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
if proj.Spec.SourceIntegrity == nil {
|
||||
log.Warnf("Creating project SourceIntegrity from legacy SignatureKeys specified in %s AppProject. Migrate them to SourceIntegrity.", proj.Name)
|
||||
return &SourceIntegrity{Git: migratedGit}
|
||||
}
|
||||
|
||||
if proj.Spec.SourceIntegrity.Git != nil {
|
||||
log.Errorf("Both SourceIntegrity and SignatureKeys specified in %s AppProject. Ignoring SignatureKeys. Migrate them to SourceIntegrity.", proj.Name)
|
||||
return proj.Spec.SourceIntegrity
|
||||
}
|
||||
|
||||
log.Warnf("Merging SourceIntegrity with legacy SignatureKeys specified in %s AppProject. Migrate them to SourceIntegrity.", proj.Name)
|
||||
// Merge with non-git checks without modifying the AppProject - use deep-copy and amend
|
||||
deepCopy := proj.Spec.SourceIntegrity.DeepCopy()
|
||||
deepCopy.Git = migratedGit
|
||||
return deepCopy
|
||||
}
|
||||
|
||||
// ClusterResourceRestrictionItem is a cluster resource that is restricted by the project's whitelist or blacklist
|
||||
|
|
|
|||
|
|
@ -2374,6 +2374,65 @@ func TestAppProjectSpec_AddWindow(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAppProject_EffectiveSourceIntegrity(t *testing.T) {
|
||||
sourceIntegrity := func(repo string, mode SourceIntegrityGitPolicyGPGMode, keys ...string) *SourceIntegrity {
|
||||
return &SourceIntegrity{
|
||||
Git: &SourceIntegrityGit{
|
||||
Policies: []*SourceIntegrityGitPolicy{{
|
||||
Repos: []SourceIntegrityGitPolicyRepo{{URL: repo}},
|
||||
GPG: &SourceIntegrityGitPolicyGPG{
|
||||
Mode: mode,
|
||||
Keys: keys,
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
spec AppProjectSpec
|
||||
expected *SourceIntegrity
|
||||
}{
|
||||
{
|
||||
name: "no old, no new",
|
||||
spec: AppProjectSpec{},
|
||||
expected: nil,
|
||||
}, {
|
||||
name: "no old, new unchanged",
|
||||
spec: AppProjectSpec{
|
||||
SourceIntegrity: sourceIntegrity("https://github.com/*", SourceIntegrityGitPolicyGPGModeStrict, "FOO"),
|
||||
},
|
||||
expected: sourceIntegrity("https://github.com/*", SourceIntegrityGitPolicyGPGModeStrict, "FOO"),
|
||||
}, {
|
||||
name: "old, no new",
|
||||
spec: AppProjectSpec{
|
||||
SignatureKeys: []SignatureKey{{"LEGACY"}, {"KEYS"}, {"FOUND"}},
|
||||
},
|
||||
expected: sourceIntegrity("*", SourceIntegrityGitPolicyGPGModeHead, "LEGACY", "KEYS", "FOUND"),
|
||||
}, {
|
||||
name: "old ignored, replaced",
|
||||
spec: AppProjectSpec{
|
||||
SignatureKeys: []SignatureKey{{"LEGACY"}, {"KEYS"}, {"FOUND"}},
|
||||
SourceIntegrity: sourceIntegrity("https://github.com/*", SourceIntegrityGitPolicyGPGModeStrict, "NEW_KEY"),
|
||||
},
|
||||
expected: sourceIntegrity("https://github.com/*", SourceIntegrityGitPolicyGPGModeStrict, "NEW_KEY"),
|
||||
}, {
|
||||
name: "old, not using Git checks",
|
||||
spec: AppProjectSpec{
|
||||
SignatureKeys: []SignatureKey{{KeyID: "LEGACY_KEY"}},
|
||||
SourceIntegrity: &SourceIntegrity{}, // Once something non-git is supported, needs checking they merge
|
||||
},
|
||||
expected: sourceIntegrity("*", SourceIntegrityGitPolicyGPGModeHead, "LEGACY_KEY"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
appProj := &AppProject{Spec: tt.spec, ObjectMeta: metav1.ObjectMeta{Name: "sut"}}
|
||||
assert.Equal(t, tt.expected, appProj.EffectiveSourceIntegrity())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppProjectSpecWindowWithDescription(t *testing.T) {
|
||||
proj := newTestProjectWithSyncWindows()
|
||||
require.NoError(t, proj.Spec.AddWindow("allow", "* * * * *", "1h", []string{"app1"}, []string{}, []string{}, false, "error", false, "Ticket AAAAA", false))
|
||||
|
|
|
|||
|
|
@ -194,6 +194,11 @@ func (in *AppProjectSpec) DeepCopyInto(out *AppProjectSpec) {
|
|||
*out = make([]ApplicationDestinationServiceAccount, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.SourceIntegrity != nil {
|
||||
in, out := &in.SourceIntegrity, &out.SourceIntegrity
|
||||
*out = new(SourceIntegrity)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -4074,6 +4079,11 @@ func (in *RevisionMetadata) DeepCopyInto(out *RevisionMetadata) {
|
|||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.SourceIntegrityResult != nil {
|
||||
in, out := &in.SourceIntegrityResult, &out.SourceIntegrityResult
|
||||
*out = new(SourceIntegrityCheckResult)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -4474,6 +4484,161 @@ func (in *SourceHydratorStatus) DeepCopy() *SourceHydratorStatus {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SourceIntegrity) DeepCopyInto(out *SourceIntegrity) {
|
||||
*out = *in
|
||||
if in.Git != nil {
|
||||
in, out := &in.Git, &out.Git
|
||||
*out = new(SourceIntegrityGit)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrity.
|
||||
func (in *SourceIntegrity) DeepCopy() *SourceIntegrity {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SourceIntegrity)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SourceIntegrityCheckResult) DeepCopyInto(out *SourceIntegrityCheckResult) {
|
||||
*out = *in
|
||||
if in.Checks != nil {
|
||||
in, out := &in.Checks, &out.Checks
|
||||
*out = make([]SourceIntegrityCheckResultItem, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityCheckResult.
|
||||
func (in *SourceIntegrityCheckResult) DeepCopy() *SourceIntegrityCheckResult {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SourceIntegrityCheckResult)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SourceIntegrityCheckResultItem) DeepCopyInto(out *SourceIntegrityCheckResultItem) {
|
||||
*out = *in
|
||||
if in.Problems != nil {
|
||||
in, out := &in.Problems, &out.Problems
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityCheckResultItem.
|
||||
func (in *SourceIntegrityCheckResultItem) DeepCopy() *SourceIntegrityCheckResultItem {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SourceIntegrityCheckResultItem)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SourceIntegrityGit) DeepCopyInto(out *SourceIntegrityGit) {
|
||||
*out = *in
|
||||
if in.Policies != nil {
|
||||
in, out := &in.Policies, &out.Policies
|
||||
*out = make([]*SourceIntegrityGitPolicy, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(SourceIntegrityGitPolicy)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityGit.
|
||||
func (in *SourceIntegrityGit) DeepCopy() *SourceIntegrityGit {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SourceIntegrityGit)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SourceIntegrityGitPolicy) DeepCopyInto(out *SourceIntegrityGitPolicy) {
|
||||
*out = *in
|
||||
if in.Repos != nil {
|
||||
in, out := &in.Repos, &out.Repos
|
||||
*out = make([]SourceIntegrityGitPolicyRepo, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.GPG != nil {
|
||||
in, out := &in.GPG, &out.GPG
|
||||
*out = new(SourceIntegrityGitPolicyGPG)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityGitPolicy.
|
||||
func (in *SourceIntegrityGitPolicy) DeepCopy() *SourceIntegrityGitPolicy {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SourceIntegrityGitPolicy)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SourceIntegrityGitPolicyGPG) DeepCopyInto(out *SourceIntegrityGitPolicyGPG) {
|
||||
*out = *in
|
||||
if in.Keys != nil {
|
||||
in, out := &in.Keys, &out.Keys
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityGitPolicyGPG.
|
||||
func (in *SourceIntegrityGitPolicyGPG) DeepCopy() *SourceIntegrityGitPolicyGPG {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SourceIntegrityGitPolicyGPG)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SourceIntegrityGitPolicyRepo) DeepCopyInto(out *SourceIntegrityGitPolicyRepo) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityGitPolicyRepo.
|
||||
func (in *SourceIntegrityGitPolicyRepo) DeepCopy() *SourceIntegrityGitPolicyRepo {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SourceIntegrityGitPolicyRepo)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SuccessfulHydrateOperation) DeepCopyInto(out *SuccessfulHydrateOperation) {
|
||||
*out = *in
|
||||
|
|
|
|||
642
reposerver/apiclient/repository.pb.go
generated
642
reposerver/apiclient/repository.pb.go
generated
|
|
@ -47,8 +47,8 @@ type ManifestRequest struct {
|
|||
KubeVersion string `protobuf:"bytes,14,opt,name=kubeVersion,proto3" json:"kubeVersion,omitempty"`
|
||||
// ApiVersions is the list of API versions from the destination cluster, used for rendering Helm charts.
|
||||
ApiVersions []string `protobuf:"bytes,15,rep,name=apiVersions,proto3" json:"apiVersions,omitempty"`
|
||||
// Request to verify the signature when generating the manifests (only for Git repositories)
|
||||
VerifySignature bool `protobuf:"varint,16,opt,name=verifySignature,proto3" json:"verifySignature,omitempty"`
|
||||
// Source integrity constrains to verify the sources before use
|
||||
SourceIntegrity *v1alpha1.SourceIntegrity `protobuf:"bytes,16,opt,name=sourceIntegrity,proto3" json:"sourceIntegrity,omitempty"`
|
||||
HelmRepoCreds []*v1alpha1.RepoCreds `protobuf:"bytes,17,rep,name=helmRepoCreds,proto3" json:"helmRepoCreds,omitempty"`
|
||||
NoRevisionCache bool `protobuf:"varint,18,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
|
||||
TrackingMethod string `protobuf:"bytes,19,opt,name=trackingMethod,proto3" json:"trackingMethod,omitempty"`
|
||||
|
|
@ -186,11 +186,11 @@ func (m *ManifestRequest) GetApiVersions() []string {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *ManifestRequest) GetVerifySignature() bool {
|
||||
func (m *ManifestRequest) GetSourceIntegrity() *v1alpha1.SourceIntegrity {
|
||||
if m != nil {
|
||||
return m.VerifySignature
|
||||
return m.SourceIntegrity
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManifestRequest) GetHelmRepoCreds() []*v1alpha1.RepoCreds {
|
||||
|
|
@ -717,12 +717,14 @@ type ManifestResponse struct {
|
|||
Revision string `protobuf:"bytes,4,opt,name=revision,proto3" json:"revision,omitempty"`
|
||||
SourceType string `protobuf:"bytes,6,opt,name=sourceType,proto3" json:"sourceType,omitempty"`
|
||||
// Raw response of git verify-commit operation (always the empty string for Helm)
|
||||
// Deprecated: Use SourceIntegrityResult for more detailed information. VerifyResult will be removed with the next major version.
|
||||
VerifyResult string `protobuf:"bytes,7,opt,name=verifyResult,proto3" json:"verifyResult,omitempty"`
|
||||
// Commands is the list of commands used to hydrate the manifests
|
||||
Commands []string `protobuf:"bytes,8,rep,name=commands,proto3" json:"commands,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
Commands []string `protobuf:"bytes,8,rep,name=commands,proto3" json:"commands,omitempty"`
|
||||
SourceIntegrityResult *v1alpha1.SourceIntegrityCheckResult `protobuf:"bytes,9,opt,name=sourceIntegrityResult,proto3" json:"sourceIntegrityResult,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ManifestResponse) Reset() { *m = ManifestResponse{} }
|
||||
|
|
@ -807,6 +809,13 @@ func (m *ManifestResponse) GetCommands() []string {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *ManifestResponse) GetSourceIntegrityResult() *v1alpha1.SourceIntegrityCheckResult {
|
||||
if m != nil {
|
||||
return m.SourceIntegrityResult
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ListRefsRequest struct {
|
||||
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
|
|
@ -1329,12 +1338,11 @@ type RepoServerRevisionMetadataRequest struct {
|
|||
// the repo
|
||||
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
|
||||
// the revision within the repo
|
||||
Revision string `protobuf:"bytes,2,opt,name=revision,proto3" json:"revision,omitempty"`
|
||||
// whether to check signature on revision
|
||||
CheckSignature bool `protobuf:"varint,3,opt,name=checkSignature,proto3" json:"checkSignature,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
Revision string `protobuf:"bytes,2,opt,name=revision,proto3" json:"revision,omitempty"`
|
||||
SourceIntegrity *v1alpha1.SourceIntegrity `protobuf:"bytes,3,opt,name=sourceIntegrity,proto3" json:"sourceIntegrity,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *RepoServerRevisionMetadataRequest) Reset() { *m = RepoServerRevisionMetadataRequest{} }
|
||||
|
|
@ -1384,11 +1392,11 @@ func (m *RepoServerRevisionMetadataRequest) GetRevision() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (m *RepoServerRevisionMetadataRequest) GetCheckSignature() bool {
|
||||
func (m *RepoServerRevisionMetadataRequest) GetSourceIntegrity() *v1alpha1.SourceIntegrity {
|
||||
if m != nil {
|
||||
return m.CheckSignature
|
||||
return m.SourceIntegrity
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
type RepoServerRevisionChartDetailsRequest struct {
|
||||
|
|
@ -1950,16 +1958,16 @@ func (m *HelmChartsResponse) GetItems() []*HelmChart {
|
|||
}
|
||||
|
||||
type GitFilesRequest struct {
|
||||
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
|
||||
SubmoduleEnabled bool `protobuf:"varint,2,opt,name=submoduleEnabled,proto3" json:"submoduleEnabled,omitempty"`
|
||||
Revision string `protobuf:"bytes,3,opt,name=revision,proto3" json:"revision,omitempty"`
|
||||
Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"`
|
||||
NewGitFileGlobbingEnabled bool `protobuf:"varint,5,opt,name=NewGitFileGlobbingEnabled,proto3" json:"NewGitFileGlobbingEnabled,omitempty"`
|
||||
NoRevisionCache bool `protobuf:"varint,6,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
|
||||
VerifyCommit bool `protobuf:"varint,7,opt,name=verifyCommit,proto3" json:"verifyCommit,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
|
||||
SubmoduleEnabled bool `protobuf:"varint,2,opt,name=submoduleEnabled,proto3" json:"submoduleEnabled,omitempty"`
|
||||
Revision string `protobuf:"bytes,3,opt,name=revision,proto3" json:"revision,omitempty"`
|
||||
Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"`
|
||||
NewGitFileGlobbingEnabled bool `protobuf:"varint,5,opt,name=NewGitFileGlobbingEnabled,proto3" json:"NewGitFileGlobbingEnabled,omitempty"`
|
||||
NoRevisionCache bool `protobuf:"varint,6,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
|
||||
SourceIntegrity *v1alpha1.SourceIntegrity `protobuf:"bytes,7,opt,name=sourceIntegrity,proto3" json:"sourceIntegrity,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *GitFilesRequest) Reset() { *m = GitFilesRequest{} }
|
||||
|
|
@ -2037,11 +2045,11 @@ func (m *GitFilesRequest) GetNoRevisionCache() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (m *GitFilesRequest) GetVerifyCommit() bool {
|
||||
func (m *GitFilesRequest) GetSourceIntegrity() *v1alpha1.SourceIntegrity {
|
||||
if m != nil {
|
||||
return m.VerifyCommit
|
||||
return m.SourceIntegrity
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
type GitFilesResponse struct {
|
||||
|
|
@ -2093,14 +2101,14 @@ func (m *GitFilesResponse) GetMap() map[string][]byte {
|
|||
}
|
||||
|
||||
type GitDirectoriesRequest struct {
|
||||
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
|
||||
SubmoduleEnabled bool `protobuf:"varint,2,opt,name=submoduleEnabled,proto3" json:"submoduleEnabled,omitempty"`
|
||||
Revision string `protobuf:"bytes,3,opt,name=revision,proto3" json:"revision,omitempty"`
|
||||
NoRevisionCache bool `protobuf:"varint,4,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
|
||||
VerifyCommit bool `protobuf:"varint,5,opt,name=verifyCommit,proto3" json:"verifyCommit,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
|
||||
SubmoduleEnabled bool `protobuf:"varint,2,opt,name=submoduleEnabled,proto3" json:"submoduleEnabled,omitempty"`
|
||||
Revision string `protobuf:"bytes,3,opt,name=revision,proto3" json:"revision,omitempty"`
|
||||
NoRevisionCache bool `protobuf:"varint,4,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
|
||||
SourceIntegrity *v1alpha1.SourceIntegrity `protobuf:"bytes,5,opt,name=sourceIntegrity,proto3" json:"sourceIntegrity,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *GitDirectoriesRequest) Reset() { *m = GitDirectoriesRequest{} }
|
||||
|
|
@ -2164,11 +2172,11 @@ func (m *GitDirectoriesRequest) GetNoRevisionCache() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (m *GitDirectoriesRequest) GetVerifyCommit() bool {
|
||||
func (m *GitDirectoriesRequest) GetSourceIntegrity() *v1alpha1.SourceIntegrity {
|
||||
if m != nil {
|
||||
return m.VerifyCommit
|
||||
return m.SourceIntegrity
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
type GitDirectoriesResponse struct {
|
||||
|
|
@ -2495,160 +2503,162 @@ func init() {
|
|||
}
|
||||
|
||||
var fileDescriptor_dd8723cfcc820480 = []byte{
|
||||
// 2447 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x1a, 0x4d, 0x6f, 0x1c, 0x49,
|
||||
0xd5, 0xf3, 0x65, 0xcf, 0x3c, 0x7f, 0x8d, 0x2b, 0xb1, 0xd3, 0x99, 0x24, 0xc6, 0xdb, 0x90, 0x28,
|
||||
0x9b, 0xec, 0x8e, 0x95, 0x44, 0xbb, 0x81, 0xec, 0xb2, 0x2b, 0xaf, 0x93, 0xd8, 0xde, 0xc4, 0x89,
|
||||
0xe9, 0x64, 0x17, 0x05, 0x02, 0xa8, 0xa6, 0xa7, 0x3c, 0xd3, 0x3b, 0xfd, 0x51, 0xe9, 0xae, 0xf6,
|
||||
0xe2, 0x48, 0x9c, 0x40, 0x5c, 0xe0, 0xc0, 0x69, 0x0f, 0xfc, 0x0f, 0xc4, 0x91, 0x13, 0x82, 0x0b,
|
||||
0x12, 0xe2, 0xc2, 0x05, 0x09, 0x94, 0x1f, 0x82, 0x50, 0x7d, 0xf4, 0xe7, 0xf4, 0x8c, 0xbd, 0x99,
|
||||
0xc4, 0x01, 0x2e, 0x76, 0x57, 0xd5, 0xab, 0xf7, 0x5e, 0xbd, 0xaf, 0x7a, 0xef, 0xd5, 0xc0, 0x25,
|
||||
0x9f, 0x50, 0x2f, 0x20, 0xfe, 0x01, 0xf1, 0xd7, 0xc5, 0xa7, 0xc5, 0x3c, 0xff, 0x30, 0xf5, 0xd9,
|
||||
0xa6, 0xbe, 0xc7, 0x3c, 0x04, 0xc9, 0x4c, 0xeb, 0x7e, 0xcf, 0x62, 0xfd, 0xb0, 0xd3, 0x36, 0x3d,
|
||||
0x67, 0x1d, 0xfb, 0x3d, 0x8f, 0xfa, 0xde, 0x17, 0xe2, 0xe3, 0x5d, 0xb3, 0xbb, 0x7e, 0x70, 0x63,
|
||||
0x9d, 0x0e, 0x7a, 0xeb, 0x98, 0x5a, 0xc1, 0x3a, 0xa6, 0xd4, 0xb6, 0x4c, 0xcc, 0x2c, 0xcf, 0x5d,
|
||||
0x3f, 0xb8, 0x86, 0x6d, 0xda, 0xc7, 0xd7, 0xd6, 0x7b, 0xc4, 0x25, 0x3e, 0x66, 0xa4, 0x2b, 0x31,
|
||||
0xb7, 0xce, 0xf5, 0x3c, 0xaf, 0x67, 0x93, 0x75, 0x31, 0xea, 0x84, 0xfb, 0xeb, 0xc4, 0xa1, 0x4c,
|
||||
0x91, 0xd5, 0xff, 0x31, 0x0f, 0x8b, 0xbb, 0xd8, 0xb5, 0xf6, 0x49, 0xc0, 0x0c, 0xf2, 0x2c, 0x24,
|
||||
0x01, 0x43, 0x4f, 0xa1, 0xca, 0x99, 0xd1, 0x4a, 0x6b, 0xa5, 0xcb, 0xb3, 0xd7, 0xb7, 0xdb, 0x09,
|
||||
0x37, 0xed, 0x88, 0x1b, 0xf1, 0xf1, 0x13, 0xb3, 0xdb, 0x3e, 0xb8, 0xd1, 0xa6, 0x83, 0x5e, 0x9b,
|
||||
0x73, 0xd3, 0x4e, 0x71, 0xd3, 0x8e, 0xb8, 0x69, 0x1b, 0xf1, 0xb1, 0x0c, 0x81, 0x15, 0xb5, 0xa0,
|
||||
0xee, 0x93, 0x03, 0x2b, 0xb0, 0x3c, 0x57, 0x2b, 0xaf, 0x95, 0x2e, 0x37, 0x8c, 0x78, 0x8c, 0x34,
|
||||
0x98, 0x71, 0xbd, 0x4d, 0x6c, 0xf6, 0x89, 0x56, 0x59, 0x2b, 0x5d, 0xae, 0x1b, 0xd1, 0x10, 0xad,
|
||||
0xc1, 0x2c, 0xa6, 0xf4, 0x3e, 0xee, 0x10, 0xfb, 0x1e, 0x39, 0xd4, 0xaa, 0x62, 0x63, 0x7a, 0x8a,
|
||||
0xef, 0xc5, 0x94, 0x3e, 0xc0, 0x0e, 0xd1, 0x6a, 0x62, 0x35, 0x1a, 0xa2, 0xf3, 0xd0, 0x70, 0xb1,
|
||||
0x43, 0x02, 0x8a, 0x4d, 0xa2, 0xd5, 0xc5, 0x5a, 0x32, 0x81, 0x7e, 0x06, 0x4b, 0x29, 0xc6, 0x1f,
|
||||
0x79, 0xa1, 0x6f, 0x12, 0x0d, 0xc4, 0xd1, 0x1f, 0x4e, 0x76, 0xf4, 0x8d, 0x3c, 0x5a, 0x63, 0x98,
|
||||
0x12, 0xfa, 0x31, 0xd4, 0x84, 0xe6, 0xb5, 0xd9, 0xb5, 0xca, 0x2b, 0x95, 0xb6, 0x44, 0x8b, 0x5c,
|
||||
0x98, 0xa1, 0x76, 0xd8, 0xb3, 0xdc, 0x40, 0x9b, 0x13, 0x14, 0x1e, 0x4f, 0x46, 0x61, 0xd3, 0x73,
|
||||
0xf7, 0xad, 0xde, 0x2e, 0x76, 0x71, 0x8f, 0x38, 0xc4, 0x65, 0x7b, 0x02, 0xb9, 0x11, 0x11, 0x41,
|
||||
0xcf, 0xa1, 0x39, 0x08, 0x03, 0xe6, 0x39, 0xd6, 0x73, 0xf2, 0x90, 0xf2, 0xbd, 0x81, 0x36, 0x2f,
|
||||
0xa4, 0xf9, 0x60, 0x32, 0xc2, 0xf7, 0x72, 0x58, 0x8d, 0x21, 0x3a, 0xdc, 0x48, 0x06, 0x61, 0x87,
|
||||
0x7c, 0x4e, 0x7c, 0x61, 0x5d, 0x0b, 0xd2, 0x48, 0x52, 0x53, 0xd2, 0x8c, 0x2c, 0x35, 0x0a, 0xb4,
|
||||
0xc5, 0xb5, 0x8a, 0x34, 0xa3, 0x78, 0x0a, 0x5d, 0x86, 0xc5, 0x03, 0xe2, 0x5b, 0xfb, 0x87, 0x8f,
|
||||
0xac, 0x9e, 0x8b, 0x59, 0xe8, 0x13, 0xad, 0x29, 0x4c, 0x31, 0x3f, 0x8d, 0x1c, 0x98, 0xef, 0x13,
|
||||
0xdb, 0xe1, 0x22, 0xdf, 0xf4, 0x49, 0x37, 0xd0, 0x96, 0x84, 0x7c, 0xb7, 0x26, 0xd7, 0xa0, 0x40,
|
||||
0x67, 0x64, 0xb1, 0x73, 0xc6, 0x5c, 0xcf, 0x50, 0x9e, 0x22, 0x7d, 0x04, 0x49, 0xc6, 0x72, 0xd3,
|
||||
0xe8, 0x12, 0x2c, 0x30, 0x1f, 0x9b, 0x03, 0xcb, 0xed, 0xed, 0x12, 0xd6, 0xf7, 0xba, 0xda, 0x29,
|
||||
0x21, 0x89, 0xdc, 0x2c, 0x32, 0x01, 0x11, 0x17, 0x77, 0x6c, 0xd2, 0x95, 0xb6, 0xf8, 0xf8, 0x90,
|
||||
0x92, 0x40, 0x3b, 0x2d, 0x4e, 0x71, 0xa3, 0x9d, 0x8a, 0x50, 0xb9, 0x00, 0xd1, 0xbe, 0x33, 0xb4,
|
||||
0xeb, 0x8e, 0xcb, 0xfc, 0x43, 0xa3, 0x00, 0x1d, 0x1a, 0xc0, 0x2c, 0x3f, 0x47, 0x64, 0x0a, 0xcb,
|
||||
0xc2, 0x14, 0x76, 0x26, 0x93, 0xd1, 0x76, 0x82, 0xd0, 0x48, 0x63, 0x47, 0x6d, 0x40, 0x7d, 0x1c,
|
||||
0xec, 0x86, 0x36, 0xb3, 0xa8, 0x4d, 0x24, 0x1b, 0x81, 0xb6, 0x22, 0xc4, 0x54, 0xb0, 0x82, 0xee,
|
||||
0x01, 0xf8, 0x64, 0x3f, 0x82, 0x3b, 0x23, 0x4e, 0x7e, 0x75, 0xdc, 0xc9, 0x8d, 0x18, 0x5a, 0x9e,
|
||||
0x38, 0xb5, 0x9d, 0x13, 0xe7, 0xc7, 0x20, 0x26, 0x53, 0xde, 0x2e, 0xdc, 0x5a, 0x13, 0x26, 0x56,
|
||||
0xb0, 0xc2, 0x6d, 0x51, 0xcd, 0x8a, 0xa0, 0x75, 0x56, 0x5a, 0x6b, 0x6a, 0x0a, 0x6d, 0xc3, 0x37,
|
||||
0xb0, 0xeb, 0x7a, 0x4c, 0x1c, 0x3f, 0x62, 0x65, 0x4b, 0x85, 0xf7, 0x3d, 0xcc, 0xfa, 0x81, 0xd6,
|
||||
0x12, 0xbb, 0x8e, 0x02, 0xe3, 0x26, 0x61, 0xb9, 0x01, 0xc3, 0xb6, 0x2d, 0x80, 0x76, 0x6e, 0x6b,
|
||||
0xe7, 0xa4, 0x49, 0x64, 0x67, 0x5b, 0x77, 0xe0, 0xcc, 0x08, 0xe5, 0xa2, 0x26, 0x54, 0x06, 0xe4,
|
||||
0x50, 0x5c, 0x0a, 0x0d, 0x83, 0x7f, 0xa2, 0xd3, 0x50, 0x3b, 0xc0, 0x76, 0x48, 0x44, 0x18, 0xaf,
|
||||
0x1b, 0x72, 0x70, 0xab, 0xfc, 0xed, 0x52, 0xeb, 0x97, 0x25, 0x58, 0xcc, 0x89, 0xaa, 0x60, 0xff,
|
||||
0x8f, 0xd2, 0xfb, 0x5f, 0x81, 0xe3, 0xec, 0x3f, 0xc6, 0x7e, 0x8f, 0xb0, 0x14, 0x23, 0xfa, 0xdf,
|
||||
0x4a, 0xa0, 0xe5, 0x74, 0xf8, 0x7d, 0x8b, 0xf5, 0xef, 0x5a, 0x36, 0x09, 0xd0, 0x4d, 0x98, 0xf1,
|
||||
0xe5, 0x9c, 0xba, 0xea, 0xce, 0x8d, 0x51, 0xfd, 0xf6, 0x94, 0x11, 0x41, 0xa3, 0x8f, 0xa0, 0xee,
|
||||
0x10, 0x86, 0xbb, 0x98, 0x61, 0xc5, 0xfb, 0x5a, 0xd1, 0x4e, 0x4e, 0x65, 0x57, 0xc1, 0x6d, 0x4f,
|
||||
0x19, 0xf1, 0x1e, 0xf4, 0x1e, 0xd4, 0xcc, 0x7e, 0xe8, 0x0e, 0xc4, 0x25, 0x37, 0x7b, 0xfd, 0xc2,
|
||||
0xa8, 0xcd, 0x9b, 0x1c, 0x68, 0x7b, 0xca, 0x90, 0xd0, 0x9f, 0x4c, 0x43, 0x95, 0x62, 0x9f, 0xe9,
|
||||
0x77, 0xe1, 0x74, 0x11, 0x09, 0x7e, 0xb3, 0x9a, 0x7d, 0x62, 0x0e, 0x82, 0xd0, 0x51, 0x62, 0x8e,
|
||||
0xc7, 0x08, 0x41, 0x35, 0xb0, 0x9e, 0x4b, 0x51, 0x57, 0x0c, 0xf1, 0xad, 0xbf, 0x0d, 0x4b, 0x43,
|
||||
0xd4, 0xb8, 0x52, 0x25, 0x6f, 0x1c, 0xc3, 0x9c, 0x22, 0xad, 0x87, 0xb0, 0xfc, 0x58, 0xc8, 0x22,
|
||||
0xbe, 0x5e, 0x4e, 0x22, 0x57, 0xd0, 0xb7, 0x61, 0x25, 0x4f, 0x36, 0xa0, 0x9e, 0x1b, 0x10, 0xee,
|
||||
0x6c, 0x22, 0x1e, 0x5b, 0xa4, 0x9b, 0xac, 0x0a, 0x2e, 0xea, 0x46, 0xc1, 0x8a, 0xfe, 0x97, 0x32,
|
||||
0xac, 0x18, 0x24, 0xf0, 0xec, 0x03, 0x12, 0x05, 0xcb, 0x93, 0x49, 0x77, 0x7e, 0x08, 0x15, 0x4c,
|
||||
0xa9, 0x32, 0x93, 0x9d, 0x57, 0x96, 0x50, 0x18, 0x1c, 0x2b, 0x7a, 0x07, 0x96, 0xb0, 0xd3, 0xb1,
|
||||
0x7a, 0xa1, 0x17, 0x06, 0xd1, 0xb1, 0x84, 0x51, 0x35, 0x8c, 0xe1, 0x05, 0x1e, 0x70, 0x02, 0xe1,
|
||||
0x91, 0x3b, 0x6e, 0x97, 0xfc, 0x54, 0xe4, 0x50, 0x15, 0x23, 0x3d, 0x55, 0x74, 0xc7, 0xd4, 0x0a,
|
||||
0xef, 0x18, 0xdd, 0x84, 0x33, 0x43, 0xe2, 0x54, 0xaa, 0x49, 0x27, 0x78, 0xa5, 0x5c, 0x82, 0x57,
|
||||
0xc8, 0x70, 0x79, 0x04, 0xc3, 0xfa, 0x8b, 0x12, 0x34, 0x13, 0x37, 0x54, 0xe8, 0xcf, 0x43, 0xc3,
|
||||
0x51, 0x73, 0x81, 0x56, 0x12, 0xd1, 0x35, 0x99, 0xc8, 0xe6, 0x7a, 0xe5, 0x7c, 0xae, 0xb7, 0x02,
|
||||
0xd3, 0x32, 0x15, 0x57, 0x42, 0x52, 0xa3, 0x0c, 0xcb, 0xd5, 0x1c, 0xcb, 0xab, 0x00, 0x41, 0x1c,
|
||||
0x0b, 0xb5, 0x69, 0xb1, 0x9a, 0x9a, 0x41, 0x3a, 0xcc, 0xc9, 0xcc, 0xc0, 0x20, 0x41, 0x68, 0x33,
|
||||
0x6d, 0x46, 0x40, 0x64, 0xe6, 0x84, 0x67, 0x7a, 0x8e, 0x83, 0xdd, 0x6e, 0xa0, 0xd5, 0x05, 0xcb,
|
||||
0xf1, 0x58, 0xf7, 0x60, 0xf1, 0xbe, 0xc5, 0xcf, 0xb7, 0x1f, 0x9c, 0x8c, 0x53, 0xbd, 0x0f, 0x55,
|
||||
0x4e, 0x8c, 0x33, 0xd5, 0xf1, 0xb1, 0x6b, 0xf6, 0x49, 0x24, 0xc7, 0x78, 0xcc, 0xc3, 0x05, 0xc3,
|
||||
0xbd, 0x40, 0x2b, 0x8b, 0x79, 0xf1, 0xad, 0xff, 0xbe, 0x2c, 0x39, 0xdd, 0xa0, 0x34, 0x78, 0xf3,
|
||||
0xa5, 0x42, 0x71, 0xf2, 0x52, 0x19, 0x4e, 0x5e, 0x72, 0x2c, 0x7f, 0x9d, 0xe4, 0xe5, 0x15, 0x5d,
|
||||
0x87, 0x7a, 0x08, 0x33, 0x1b, 0x94, 0x72, 0x46, 0xd0, 0x35, 0xa8, 0x62, 0x4a, 0xa5, 0xc0, 0x73,
|
||||
0x91, 0x5f, 0x81, 0xf0, 0xff, 0x8a, 0x25, 0x01, 0xda, 0xba, 0x09, 0x8d, 0x78, 0xea, 0x28, 0xb2,
|
||||
0x8d, 0x34, 0xd9, 0x35, 0x00, 0x99, 0x9d, 0xef, 0xb8, 0xfb, 0x1e, 0x57, 0x29, 0x77, 0x04, 0xb5,
|
||||
0x55, 0x7c, 0xeb, 0xb7, 0x22, 0x08, 0xc1, 0xdb, 0x3b, 0x50, 0xb3, 0x18, 0x71, 0x22, 0xe6, 0x56,
|
||||
0xd2, 0xcc, 0x25, 0x88, 0x0c, 0x09, 0xa4, 0xff, 0xa9, 0x0e, 0x67, 0xb9, 0xc6, 0x1e, 0x09, 0x17,
|
||||
0xda, 0xa0, 0xf4, 0x36, 0x61, 0xd8, 0xb2, 0x83, 0xef, 0x85, 0xc4, 0x3f, 0x7c, 0xcd, 0x86, 0xd1,
|
||||
0x83, 0x69, 0xe9, 0x81, 0x2a, 0xae, 0xbe, 0xf2, 0x42, 0x4d, 0xa1, 0x4f, 0xaa, 0xb3, 0xca, 0xeb,
|
||||
0xa9, 0xce, 0x8a, 0xaa, 0xa5, 0xea, 0x09, 0x55, 0x4b, 0xa3, 0x0b, 0xe6, 0x54, 0x19, 0x3e, 0x9d,
|
||||
0x2d, 0xc3, 0x0b, 0x2e, 0x88, 0x99, 0xe3, 0x16, 0x21, 0xf5, 0xc2, 0x22, 0xc4, 0x29, 0xf4, 0xe3,
|
||||
0x86, 0x10, 0xf7, 0x77, 0xd3, 0x16, 0x38, 0xd2, 0xd6, 0x26, 0x29, 0x47, 0xe0, 0xb5, 0x96, 0x23,
|
||||
0x9f, 0x65, 0xca, 0x0b, 0x59, 0xe0, 0xbf, 0x77, 0xbc, 0x33, 0x8d, 0x29, 0x34, 0xfe, 0xef, 0x92,
|
||||
0xf4, 0x5f, 0x88, 0xdc, 0x8c, 0x7a, 0x89, 0x0c, 0xe2, 0xcb, 0x9e, 0xdf, 0x43, 0xfc, 0xda, 0x55,
|
||||
0x41, 0x8b, 0x7f, 0xa3, 0xab, 0x50, 0xe5, 0x42, 0x56, 0xc9, 0xf3, 0x99, 0xb4, 0x3c, 0xb9, 0x26,
|
||||
0x36, 0x28, 0x7d, 0x44, 0x89, 0x69, 0x08, 0x20, 0x74, 0x0b, 0x1a, 0xb1, 0xe1, 0x2b, 0xcf, 0x3a,
|
||||
0x9f, 0xde, 0x11, 0xfb, 0x49, 0xb4, 0x2d, 0x01, 0xe7, 0x7b, 0xbb, 0x96, 0x4f, 0x4c, 0x91, 0x5a,
|
||||
0xd6, 0x86, 0xf7, 0xde, 0x8e, 0x16, 0xe3, 0xbd, 0x31, 0x38, 0xba, 0x06, 0xd3, 0xb2, 0x23, 0x22,
|
||||
0x3c, 0x68, 0xf6, 0xfa, 0xd9, 0xe1, 0x60, 0x1a, 0xed, 0x52, 0x80, 0xfa, 0x1f, 0x4b, 0xf0, 0x56,
|
||||
0x62, 0x10, 0x91, 0x37, 0x45, 0xd9, 0xfd, 0x9b, 0xbf, 0x71, 0x2f, 0xc1, 0x82, 0x28, 0x27, 0x92,
|
||||
0xc6, 0x88, 0xec, 0xd1, 0xe5, 0x66, 0xf5, 0xdf, 0x95, 0xe0, 0xe2, 0xf0, 0x39, 0x36, 0xfb, 0xd8,
|
||||
0x67, 0xb1, 0x7a, 0x4f, 0xe2, 0x2c, 0xd1, 0x85, 0x57, 0x4e, 0x2e, 0xbc, 0xcc, 0xf9, 0x2a, 0xd9,
|
||||
0xf3, 0xe9, 0x7f, 0x28, 0xc3, 0x6c, 0xca, 0x80, 0x8a, 0x2e, 0x4c, 0x9e, 0x0c, 0x0a, 0xbb, 0x15,
|
||||
0x05, 0xa4, 0xb8, 0x14, 0x1a, 0x46, 0x6a, 0x06, 0x0d, 0x00, 0x28, 0xf6, 0xb1, 0x43, 0x18, 0xf1,
|
||||
0x79, 0x24, 0xe7, 0x1e, 0x7f, 0x6f, 0xf2, 0xe8, 0xb2, 0x17, 0xe1, 0x34, 0x52, 0xe8, 0x79, 0x36,
|
||||
0x2b, 0x48, 0x07, 0x2a, 0x7e, 0xab, 0x11, 0xfa, 0x12, 0x16, 0xf6, 0x2d, 0x9b, 0xec, 0x25, 0x8c,
|
||||
0x4c, 0x0b, 0x46, 0x1e, 0x4e, 0xce, 0xc8, 0xdd, 0x34, 0x5e, 0x23, 0x47, 0x46, 0xbf, 0x02, 0xcd,
|
||||
0xbc, 0x3f, 0x71, 0x26, 0x2d, 0x07, 0xf7, 0x62, 0x69, 0xa9, 0x91, 0x8e, 0xa0, 0x99, 0xf7, 0x1f,
|
||||
0xfd, 0x9f, 0x65, 0x58, 0x8e, 0xd1, 0x6d, 0xb8, 0xae, 0x17, 0xba, 0xa6, 0x68, 0x32, 0x16, 0xea,
|
||||
0xe2, 0x34, 0xd4, 0x98, 0xc5, 0xec, 0x38, 0xf1, 0x11, 0x03, 0x7e, 0x77, 0x31, 0xcf, 0xb3, 0x99,
|
||||
0x45, 0x95, 0x82, 0xa3, 0xa1, 0xd4, 0xfd, 0xb3, 0xd0, 0xf2, 0x49, 0x57, 0x44, 0x82, 0xba, 0x11,
|
||||
0x8f, 0xf9, 0x1a, 0xcf, 0x6a, 0x44, 0x8a, 0x2f, 0x85, 0x19, 0x8f, 0x85, 0xdd, 0x7b, 0xb6, 0x4d,
|
||||
0x4c, 0x2e, 0x8e, 0x54, 0x11, 0x90, 0x9b, 0x15, 0xc5, 0x05, 0xf3, 0x2d, 0xb7, 0xa7, 0x4a, 0x00,
|
||||
0x35, 0xe2, 0x7c, 0x62, 0xdf, 0xc7, 0x87, 0x2a, 0xf3, 0x97, 0x03, 0xf4, 0x21, 0x54, 0x1c, 0x4c,
|
||||
0xd5, 0x45, 0x77, 0x25, 0x13, 0x1d, 0x8a, 0x24, 0xd0, 0xde, 0xc5, 0x54, 0xde, 0x04, 0x7c, 0x5b,
|
||||
0xeb, 0x7d, 0xa8, 0x47, 0x13, 0x5f, 0x2b, 0x25, 0xfc, 0x02, 0xe6, 0x33, 0xc1, 0x07, 0x3d, 0x81,
|
||||
0x95, 0xc4, 0xa2, 0xd2, 0x04, 0x55, 0x12, 0xf8, 0xd6, 0x91, 0x9c, 0x19, 0x23, 0x10, 0xe8, 0xcf,
|
||||
0x60, 0x89, 0x9b, 0x8c, 0x70, 0xfc, 0x13, 0x2a, 0x6d, 0x3e, 0x80, 0x46, 0x4c, 0xb2, 0xd0, 0x66,
|
||||
0x5a, 0x50, 0x3f, 0x88, 0x9a, 0xbf, 0xb2, 0xb6, 0x89, 0xc7, 0xfa, 0x06, 0xa0, 0x34, 0xbf, 0xea,
|
||||
0x06, 0xba, 0x9a, 0x4d, 0x8a, 0x97, 0xf3, 0xd7, 0x8d, 0x00, 0x8f, 0x72, 0xe2, 0xbf, 0x97, 0x61,
|
||||
0x71, 0xcb, 0x12, 0xdd, 0x94, 0x13, 0x0a, 0x72, 0x57, 0xa0, 0x19, 0x84, 0x1d, 0xc7, 0xeb, 0x86,
|
||||
0x36, 0x51, 0x49, 0x81, 0xba, 0xe9, 0x87, 0xe6, 0xc7, 0x05, 0x3f, 0x2e, 0x2c, 0x8a, 0x59, 0x5f,
|
||||
0x55, 0xbf, 0xe2, 0x1b, 0x7d, 0x08, 0x67, 0x1f, 0x90, 0x2f, 0xd5, 0x79, 0xb6, 0x6c, 0xaf, 0xd3,
|
||||
0xb1, 0xdc, 0x5e, 0x44, 0x44, 0xf6, 0x05, 0x46, 0x03, 0x14, 0xa5, 0x8a, 0xd3, 0xc5, 0xa9, 0x62,
|
||||
0x5c, 0x41, 0x6f, 0x7a, 0x8e, 0x63, 0x31, 0x95, 0x51, 0x66, 0xe6, 0xf4, 0x9f, 0x97, 0xa0, 0x99,
|
||||
0x48, 0x56, 0xe9, 0xe6, 0xa6, 0xf4, 0x21, 0xa9, 0x99, 0x8b, 0x69, 0xcd, 0xe4, 0x41, 0x5f, 0xde,
|
||||
0x7d, 0xe6, 0xd2, 0xee, 0xf3, 0xab, 0x32, 0x2c, 0x6f, 0x59, 0x2c, 0x0a, 0x5c, 0xd6, 0xff, 0x9a,
|
||||
0x96, 0x0b, 0x74, 0x52, 0x3d, 0x9e, 0x4e, 0x6a, 0x05, 0x3a, 0x69, 0xc3, 0x4a, 0x5e, 0x18, 0x4a,
|
||||
0x31, 0xa7, 0xa1, 0x46, 0x45, 0x7b, 0x5a, 0xf6, 0x15, 0xe4, 0x40, 0xff, 0x77, 0x1d, 0x2e, 0x7c,
|
||||
0x46, 0xbb, 0x98, 0xc5, 0x3d, 0xa3, 0xbb, 0x9e, 0x2f, 0xfa, 0xd3, 0x27, 0x23, 0xc5, 0xdc, 0x1b,
|
||||
0x62, 0x79, 0xec, 0x1b, 0x62, 0x65, 0xcc, 0x1b, 0x62, 0xf5, 0x58, 0x6f, 0x88, 0xb5, 0x13, 0x7b,
|
||||
0x43, 0x1c, 0xae, 0xb5, 0xa6, 0x0b, 0x6b, 0xad, 0x27, 0x99, 0x7a, 0x64, 0x46, 0xb8, 0xcd, 0x77,
|
||||
0xd2, 0x6e, 0x33, 0x56, 0x3b, 0x63, 0x1f, 0x3f, 0x72, 0x4f, 0x6f, 0xf5, 0x23, 0x9f, 0xde, 0x1a,
|
||||
0xc3, 0x4f, 0x6f, 0xc5, 0xaf, 0x37, 0x30, 0xf2, 0xf5, 0xe6, 0x12, 0x2c, 0x04, 0x87, 0xae, 0x49,
|
||||
0xba, 0x71, 0x27, 0x71, 0x56, 0x1e, 0x3b, 0x3b, 0x9b, 0xf1, 0x88, 0xb9, 0x9c, 0x47, 0xc4, 0x96,
|
||||
0x3a, 0x9f, 0xb2, 0xd4, 0x22, 0x3f, 0x59, 0x18, 0x59, 0xe6, 0xe6, 0x1e, 0x56, 0x16, 0x8b, 0x1e,
|
||||
0x56, 0xd0, 0x00, 0x9a, 0x11, 0x57, 0xb1, 0x02, 0x9a, 0x42, 0x01, 0x1f, 0x1f, 0x5f, 0x01, 0x8f,
|
||||
0x72, 0x18, 0xa4, 0x1a, 0x86, 0x10, 0xff, 0xd7, 0x54, 0x76, 0xad, 0x5f, 0x97, 0x60, 0xb9, 0x90,
|
||||
0xe9, 0x37, 0x53, 0x68, 0x7e, 0x0e, 0xab, 0xa3, 0x04, 0xac, 0x02, 0x97, 0x06, 0x33, 0x66, 0x1f,
|
||||
0xbb, 0x3d, 0xd1, 0x12, 0x15, 0x9d, 0x0f, 0x35, 0x1c, 0x57, 0x19, 0x5d, 0xff, 0x6a, 0x0e, 0x96,
|
||||
0x92, 0x8a, 0x87, 0xff, 0xb5, 0x4c, 0x82, 0x1e, 0x42, 0x33, 0x7a, 0x84, 0x8b, 0x9a, 0xd8, 0x68,
|
||||
0xdc, 0x0b, 0x53, 0xeb, 0x7c, 0xf1, 0xa2, 0x64, 0x4d, 0x9f, 0x42, 0x26, 0x9c, 0xcd, 0x23, 0x4c,
|
||||
0x1e, 0xb3, 0xbe, 0x35, 0x06, 0x73, 0x0c, 0x75, 0x14, 0x89, 0xcb, 0x25, 0xf4, 0x04, 0x16, 0xb2,
|
||||
0x4f, 0x2e, 0x28, 0x93, 0x02, 0x16, 0xbe, 0x02, 0xb5, 0xf4, 0x71, 0x20, 0x31, 0xff, 0x4f, 0xb9,
|
||||
0x55, 0x66, 0xde, 0x0c, 0x90, 0x9e, 0xed, 0x86, 0x14, 0xbd, 0xcf, 0xb4, 0xbe, 0x39, 0x16, 0x26,
|
||||
0xc6, 0xfe, 0x01, 0xd4, 0xa3, 0x3e, 0x7a, 0x56, 0xcc, 0xb9, 0xee, 0x7a, 0xab, 0x99, 0xc5, 0xb7,
|
||||
0x1f, 0xe8, 0x53, 0xe8, 0x23, 0x98, 0xe5, 0x60, 0x0f, 0x37, 0x77, 0x1e, 0xe3, 0xde, 0x4b, 0xed,
|
||||
0xaf, 0x47, 0x7d, 0xe6, 0xe1, 0xcd, 0xa9, 0xee, 0x73, 0xeb, 0x54, 0x41, 0xc7, 0x57, 0x9f, 0x42,
|
||||
0x1f, 0x4b, 0xfa, 0x7b, 0xea, 0x47, 0x14, 0x2b, 0x6d, 0xf9, 0x9b, 0x9d, 0x76, 0xf4, 0x9b, 0x9d,
|
||||
0xf6, 0x1d, 0x87, 0xb2, 0xc3, 0x56, 0x41, 0x4b, 0x56, 0x21, 0x78, 0x0a, 0xf3, 0x5b, 0x84, 0x25,
|
||||
0x1d, 0x14, 0x74, 0xf1, 0x58, 0x7d, 0xa6, 0x96, 0x9e, 0x07, 0x1b, 0x6e, 0xc2, 0xe8, 0x53, 0xe8,
|
||||
0xab, 0x12, 0x9c, 0xda, 0x22, 0x2c, 0xdf, 0x93, 0x40, 0xef, 0x16, 0x13, 0x19, 0xd1, 0xbb, 0x68,
|
||||
0x3d, 0x98, 0xd4, 0xa7, 0xb3, 0x68, 0xf5, 0x29, 0xf4, 0x9b, 0x12, 0x2c, 0x6c, 0x11, 0xae, 0xb7,
|
||||
0x98, 0xa7, 0x6b, 0xe3, 0x79, 0x2a, 0xe8, 0x43, 0xb4, 0x26, 0xec, 0xff, 0xa5, 0xa8, 0xeb, 0x53,
|
||||
0xe8, 0xb7, 0x25, 0x38, 0x93, 0x92, 0x55, 0x9a, 0xde, 0xcb, 0xf0, 0xf6, 0xe9, 0x84, 0x3f, 0xd7,
|
||||
0x49, 0xa1, 0xd4, 0xa7, 0xd0, 0x9e, 0x30, 0x93, 0xa4, 0xcc, 0x41, 0x17, 0x0a, 0xeb, 0x99, 0x98,
|
||||
0xfa, 0xea, 0xa8, 0xe5, 0xd8, 0x34, 0x3e, 0x85, 0xd9, 0x2d, 0xc2, 0xa2, 0x7c, 0x3b, 0x6b, 0xfc,
|
||||
0xb9, 0x52, 0x28, 0x1b, 0x7d, 0xf2, 0x29, 0xba, 0x30, 0xe2, 0x25, 0x89, 0x2b, 0x95, 0x53, 0x66,
|
||||
0xc3, 0x4f, 0x61, 0xf2, 0x9d, 0x35, 0xe2, 0xe2, 0x94, 0x54, 0x9f, 0x42, 0xcf, 0x60, 0xa5, 0x38,
|
||||
0xfa, 0xa3, 0xb7, 0x8f, 0x7d, 0x05, 0xb7, 0xae, 0x1c, 0x07, 0x34, 0x22, 0xf9, 0xc9, 0xc6, 0x9f,
|
||||
0x5f, 0xac, 0x96, 0xfe, 0xfa, 0x62, 0xb5, 0xf4, 0xaf, 0x17, 0xab, 0xa5, 0x1f, 0xdc, 0x38, 0xe2,
|
||||
0x67, 0x7d, 0xa9, 0x5f, 0x0a, 0x62, 0x6a, 0x99, 0xb6, 0x45, 0x5c, 0xd6, 0x99, 0x16, 0x21, 0xe0,
|
||||
0xc6, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x3f, 0x5c, 0xe7, 0x38, 0x48, 0x28, 0x00, 0x00,
|
||||
// 2471 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x1a, 0x4d, 0x73, 0x1c, 0x47,
|
||||
0x75, 0x3f, 0xa5, 0xdd, 0x27, 0x59, 0x5a, 0x75, 0x2c, 0x79, 0xbc, 0xb6, 0x85, 0x32, 0x60, 0x97,
|
||||
0x63, 0x27, 0xab, 0xb2, 0x5d, 0x89, 0xc1, 0x09, 0x49, 0x29, 0xb2, 0x2d, 0x29, 0xb6, 0x6c, 0x31,
|
||||
0x56, 0x02, 0x06, 0x03, 0xd5, 0x3b, 0xdb, 0x9a, 0x9d, 0xec, 0x7c, 0xb4, 0x67, 0x7a, 0x64, 0xd6,
|
||||
0x55, 0x9c, 0xa0, 0x38, 0x51, 0x14, 0x27, 0x1f, 0xf8, 0x0b, 0x70, 0xa5, 0x38, 0x72, 0x84, 0x0b,
|
||||
0x55, 0x14, 0x7f, 0x80, 0x94, 0xff, 0x02, 0x77, 0x8a, 0xea, 0x9e, 0x9e, 0xd9, 0x99, 0xd9, 0xd9,
|
||||
0x95, 0xe2, 0x95, 0xd7, 0xc0, 0x45, 0x9a, 0xfe, 0x7a, 0xef, 0xf5, 0xfb, 0x7e, 0xaf, 0x17, 0x2e,
|
||||
0x79, 0x84, 0xba, 0x3e, 0xf1, 0x0e, 0x89, 0xb7, 0x2e, 0x3e, 0x4d, 0xe6, 0x7a, 0xfd, 0xc4, 0x67,
|
||||
0x8b, 0x7a, 0x2e, 0x73, 0x11, 0x0c, 0x66, 0x9a, 0xf7, 0x0d, 0x93, 0x75, 0x83, 0x76, 0x4b, 0x77,
|
||||
0xed, 0x75, 0xec, 0x19, 0x2e, 0xf5, 0xdc, 0x2f, 0xc5, 0xc7, 0x7b, 0x7a, 0x67, 0xfd, 0xf0, 0xc6,
|
||||
0x3a, 0xed, 0x19, 0xeb, 0x98, 0x9a, 0xfe, 0x3a, 0xa6, 0xd4, 0x32, 0x75, 0xcc, 0x4c, 0xd7, 0x59,
|
||||
0x3f, 0xbc, 0x86, 0x2d, 0xda, 0xc5, 0xd7, 0xd6, 0x0d, 0xe2, 0x10, 0x0f, 0x33, 0xd2, 0x09, 0x21,
|
||||
0x37, 0xcf, 0x19, 0xae, 0x6b, 0x58, 0x64, 0x5d, 0x8c, 0xda, 0xc1, 0xc1, 0x3a, 0xb1, 0x29, 0x93,
|
||||
0x68, 0xd5, 0xdf, 0x2f, 0xc0, 0xe2, 0x2e, 0x76, 0xcc, 0x03, 0xe2, 0x33, 0x8d, 0x3c, 0x0d, 0x88,
|
||||
0xcf, 0xd0, 0x13, 0xa8, 0x70, 0x62, 0x94, 0xe2, 0x5a, 0xf1, 0xf2, 0xdc, 0xf5, 0xed, 0xd6, 0x80,
|
||||
0x9a, 0x56, 0x44, 0x8d, 0xf8, 0xf8, 0xa9, 0xde, 0x69, 0x1d, 0xde, 0x68, 0xd1, 0x9e, 0xd1, 0xe2,
|
||||
0xd4, 0xb4, 0x12, 0xd4, 0xb4, 0x22, 0x6a, 0x5a, 0x5a, 0x7c, 0x2d, 0x4d, 0x40, 0x45, 0x4d, 0xa8,
|
||||
0x79, 0xe4, 0xd0, 0xf4, 0x4d, 0xd7, 0x51, 0x4a, 0x6b, 0xc5, 0xcb, 0x75, 0x2d, 0x1e, 0x23, 0x05,
|
||||
0x66, 0x1d, 0x77, 0x13, 0xeb, 0x5d, 0xa2, 0x94, 0xd7, 0x8a, 0x97, 0x6b, 0x5a, 0x34, 0x44, 0x6b,
|
||||
0x30, 0x87, 0x29, 0xbd, 0x8f, 0xdb, 0xc4, 0xba, 0x47, 0xfa, 0x4a, 0x45, 0x1c, 0x4c, 0x4e, 0xf1,
|
||||
0xb3, 0x98, 0xd2, 0x07, 0xd8, 0x26, 0x4a, 0x55, 0xac, 0x46, 0x43, 0x74, 0x1e, 0xea, 0x0e, 0xb6,
|
||||
0x89, 0x4f, 0xb1, 0x4e, 0x94, 0x9a, 0x58, 0x1b, 0x4c, 0xa0, 0x9f, 0xc3, 0x52, 0x82, 0xf0, 0x47,
|
||||
0x6e, 0xe0, 0xe9, 0x44, 0x01, 0x71, 0xf5, 0x87, 0x93, 0x5d, 0x7d, 0x23, 0x0b, 0x56, 0x1b, 0xc6,
|
||||
0x84, 0x7e, 0x02, 0x55, 0x21, 0x79, 0x65, 0x6e, 0xad, 0x7c, 0xa2, 0xdc, 0x0e, 0xc1, 0x22, 0x07,
|
||||
0x66, 0xa9, 0x15, 0x18, 0xa6, 0xe3, 0x2b, 0xf3, 0x02, 0xc3, 0xfe, 0x64, 0x18, 0x36, 0x5d, 0xe7,
|
||||
0xc0, 0x34, 0x76, 0xb1, 0x83, 0x0d, 0x62, 0x13, 0x87, 0xed, 0x09, 0xe0, 0x5a, 0x84, 0x04, 0x3d,
|
||||
0x87, 0x46, 0x2f, 0xf0, 0x99, 0x6b, 0x9b, 0xcf, 0xc9, 0x43, 0xca, 0xcf, 0xfa, 0xca, 0x29, 0xc1,
|
||||
0xcd, 0x07, 0x93, 0x21, 0xbe, 0x97, 0x81, 0xaa, 0x0d, 0xe1, 0xe1, 0x4a, 0xd2, 0x0b, 0xda, 0xe4,
|
||||
0x0b, 0xe2, 0x09, 0xed, 0x5a, 0x08, 0x95, 0x24, 0x31, 0x15, 0xaa, 0x91, 0x29, 0x47, 0xbe, 0xb2,
|
||||
0xb8, 0x56, 0x0e, 0xd5, 0x28, 0x9e, 0x42, 0xcf, 0x60, 0xd1, 0x17, 0x92, 0xd9, 0x71, 0x18, 0x31,
|
||||
0x3c, 0x93, 0xf5, 0x95, 0x86, 0x20, 0x7f, 0x77, 0x32, 0xf2, 0x1f, 0xa5, 0x81, 0x6a, 0x59, 0x2c,
|
||||
0xc8, 0x86, 0x53, 0x5d, 0x62, 0xd9, 0x5c, 0x82, 0x9b, 0x1e, 0xe9, 0xf8, 0xca, 0x92, 0x10, 0xd7,
|
||||
0xd6, 0xe4, 0x0a, 0x21, 0xc0, 0x69, 0x69, 0xe8, 0xe8, 0x32, 0x2c, 0x3a, 0xae, 0x26, 0x0d, 0x2f,
|
||||
0x34, 0x39, 0x24, 0x4c, 0x2e, 0x3b, 0x8d, 0x2e, 0xc1, 0x02, 0xf3, 0xb0, 0xde, 0x33, 0x1d, 0x63,
|
||||
0x97, 0xb0, 0xae, 0xdb, 0x51, 0xde, 0x12, 0x8c, 0xcd, 0xcc, 0x22, 0x1d, 0x10, 0x71, 0x70, 0xdb,
|
||||
0x22, 0x9d, 0xf0, 0xae, 0xfb, 0x7d, 0x4a, 0x7c, 0xe5, 0xb4, 0xb8, 0xc5, 0x8d, 0x56, 0xc2, 0xe1,
|
||||
0x65, 0xfc, 0x4d, 0xeb, 0xce, 0xd0, 0xa9, 0x3b, 0x0e, 0xf3, 0xfa, 0x5a, 0x0e, 0x38, 0xd4, 0x83,
|
||||
0x39, 0x7e, 0x8f, 0x48, 0xb3, 0x96, 0x85, 0x68, 0x76, 0x26, 0xe3, 0xd1, 0xf6, 0x00, 0xa0, 0x96,
|
||||
0x84, 0x8e, 0x5a, 0x80, 0xba, 0xd8, 0xdf, 0x0d, 0x2c, 0x66, 0x52, 0x8b, 0x84, 0x64, 0xf8, 0xca,
|
||||
0x8a, 0x60, 0x53, 0xce, 0x0a, 0xba, 0x07, 0xe0, 0x91, 0x83, 0x68, 0xdf, 0x19, 0x71, 0xf3, 0xab,
|
||||
0xe3, 0x6e, 0xae, 0xc5, 0xbb, 0xc3, 0x1b, 0x27, 0x8e, 0x73, 0xe4, 0xfc, 0x1a, 0x44, 0x67, 0xd2,
|
||||
0x79, 0x08, 0x2f, 0xa1, 0x08, 0x8d, 0xcd, 0x59, 0xe1, 0xaa, 0x2d, 0x67, 0x85, 0x0f, 0x3c, 0x1b,
|
||||
0x2a, 0x7f, 0x62, 0x0a, 0x6d, 0xc3, 0x37, 0xb0, 0xe3, 0xb8, 0x4c, 0x5c, 0x3f, 0x22, 0x65, 0x4b,
|
||||
0x46, 0x8b, 0x3d, 0xcc, 0xba, 0xbe, 0xd2, 0x14, 0xa7, 0x8e, 0xda, 0xc6, 0x55, 0xc2, 0x74, 0x7c,
|
||||
0x86, 0x2d, 0x4b, 0x6c, 0xda, 0xb9, 0xad, 0x9c, 0x0b, 0x55, 0x22, 0x3d, 0xdb, 0xbc, 0x03, 0x67,
|
||||
0x46, 0x08, 0x17, 0x35, 0xa0, 0xdc, 0x23, 0x7d, 0x11, 0x63, 0xea, 0x1a, 0xff, 0x44, 0xa7, 0xa1,
|
||||
0x7a, 0x88, 0xad, 0x80, 0x88, 0xa8, 0x50, 0xd3, 0xc2, 0xc1, 0xad, 0xd2, 0xb7, 0x8b, 0xcd, 0x5f,
|
||||
0x15, 0x61, 0x31, 0xc3, 0xaa, 0x9c, 0xf3, 0x3f, 0x4e, 0x9e, 0x3f, 0x01, 0xc3, 0x39, 0xd8, 0xc7,
|
||||
0x9e, 0x41, 0x58, 0x82, 0x10, 0xf5, 0x1f, 0x45, 0x50, 0x32, 0x32, 0xfc, 0xbe, 0xc9, 0xba, 0x77,
|
||||
0x4d, 0x8b, 0xf8, 0xe8, 0x26, 0xcc, 0x7a, 0xe1, 0x9c, 0x8c, 0x9c, 0xe7, 0xc6, 0x88, 0x7e, 0xbb,
|
||||
0xa0, 0x45, 0xbb, 0xd1, 0xc7, 0x50, 0xb3, 0x09, 0xc3, 0x1d, 0xcc, 0xb0, 0xa4, 0x7d, 0x2d, 0xef,
|
||||
0x24, 0xc7, 0xb2, 0x2b, 0xf7, 0x6d, 0x17, 0xb4, 0xf8, 0x0c, 0x7a, 0x1f, 0xaa, 0x7a, 0x37, 0x70,
|
||||
0x7a, 0x22, 0x66, 0xce, 0x5d, 0xbf, 0x30, 0xea, 0xf0, 0x26, 0xdf, 0xb4, 0x5d, 0xd0, 0xc2, 0xdd,
|
||||
0x9f, 0xce, 0x40, 0x85, 0x62, 0x8f, 0xa9, 0x77, 0xe1, 0x74, 0x1e, 0x0a, 0x1e, 0xa8, 0xf5, 0x2e,
|
||||
0xd1, 0x7b, 0x7e, 0x60, 0x4b, 0x36, 0xc7, 0x63, 0x84, 0xa0, 0xe2, 0x9b, 0xcf, 0x43, 0x56, 0x97,
|
||||
0x35, 0xf1, 0xad, 0xbe, 0x03, 0x4b, 0x43, 0xd8, 0xb8, 0x50, 0x43, 0xda, 0x38, 0x84, 0x79, 0x89,
|
||||
0x5a, 0x0d, 0x60, 0x79, 0x5f, 0xf0, 0x22, 0x8e, 0x56, 0xd3, 0x48, 0x3d, 0xd4, 0x6d, 0x58, 0xc9,
|
||||
0xa2, 0xf5, 0xa9, 0xeb, 0xf8, 0x84, 0x1b, 0xdb, 0x21, 0xf1, 0xcc, 0x03, 0x93, 0x74, 0x06, 0xab,
|
||||
0x82, 0x8a, 0x9a, 0x96, 0xb3, 0xa2, 0xfe, 0xad, 0x04, 0x2b, 0x1a, 0xf1, 0x5d, 0xeb, 0x90, 0x44,
|
||||
0xce, 0x72, 0x3a, 0xd9, 0xd3, 0x8f, 0xa0, 0x8c, 0x29, 0x95, 0x6a, 0xb2, 0x73, 0x62, 0xf9, 0x89,
|
||||
0xc6, 0xa1, 0xa2, 0x77, 0x61, 0x09, 0xdb, 0x6d, 0xd3, 0x08, 0xdc, 0xc0, 0x8f, 0xae, 0x25, 0x94,
|
||||
0xaa, 0xae, 0x0d, 0x2f, 0x70, 0x87, 0x13, 0xc5, 0xb0, 0x0e, 0xf9, 0x99, 0x48, 0xc9, 0xca, 0x5a,
|
||||
0x72, 0x2a, 0x2f, 0xc6, 0x54, 0x73, 0x63, 0x8c, 0xaa, 0xc3, 0x99, 0x21, 0x76, 0x4a, 0xd1, 0x24,
|
||||
0xf3, 0xc5, 0x62, 0x26, 0x5f, 0xcc, 0x25, 0xb8, 0x34, 0x82, 0x60, 0xf5, 0x5f, 0x25, 0x68, 0x0c,
|
||||
0xcc, 0x50, 0x82, 0x3f, 0x0f, 0x75, 0x5b, 0xce, 0xf9, 0x4a, 0x51, 0x78, 0xd7, 0xc1, 0x44, 0x3a,
|
||||
0x75, 0x2c, 0x65, 0x53, 0xc7, 0x15, 0x98, 0x09, 0x33, 0x7b, 0xc9, 0x24, 0x39, 0x4a, 0x91, 0x5c,
|
||||
0xc9, 0x90, 0xbc, 0x0a, 0xe0, 0xc7, 0xbe, 0x50, 0x99, 0x11, 0xab, 0x89, 0x19, 0xa4, 0xc2, 0xbc,
|
||||
0xd0, 0x37, 0xae, 0x9b, 0x81, 0xc5, 0x94, 0x59, 0xb1, 0x23, 0x35, 0x27, 0x2c, 0xd3, 0xb5, 0x6d,
|
||||
0xec, 0x74, 0x7c, 0xa5, 0x26, 0x48, 0x8e, 0xc7, 0xe8, 0x37, 0x45, 0x58, 0xce, 0xa4, 0x16, 0x12,
|
||||
0x52, 0x5d, 0xe8, 0xcc, 0x0f, 0x4e, 0x34, 0x8d, 0xd9, 0xe4, 0x0e, 0x21, 0x84, 0xaf, 0xe5, 0xa3,
|
||||
0x55, 0x5d, 0x58, 0xbc, 0x6f, 0x72, 0x86, 0x1f, 0xf8, 0xd3, 0xb1, 0xf2, 0x0f, 0xa0, 0xc2, 0x91,
|
||||
0x71, 0x2e, 0xb5, 0x3d, 0xec, 0xe8, 0x5d, 0x12, 0x09, 0x36, 0x1e, 0x73, 0xff, 0xc5, 0xb0, 0xe1,
|
||||
0x2b, 0x25, 0x31, 0x2f, 0xbe, 0xd5, 0x3f, 0x95, 0x42, 0x4a, 0x37, 0x28, 0xf5, 0xdf, 0x7c, 0x29,
|
||||
0x94, 0x9f, 0x4d, 0x95, 0x87, 0xb3, 0xa9, 0x0c, 0xc9, 0x5f, 0x27, 0x9b, 0x3a, 0xa1, 0xf8, 0xac,
|
||||
0x06, 0x30, 0xbb, 0x41, 0x29, 0x27, 0x04, 0x5d, 0x83, 0x0a, 0xa6, 0x34, 0x64, 0x78, 0x26, 0x14,
|
||||
0xc9, 0x2d, 0xfc, 0xbf, 0x24, 0x49, 0x6c, 0x6d, 0xde, 0x84, 0x7a, 0x3c, 0x75, 0x14, 0xda, 0x7a,
|
||||
0x12, 0xed, 0x1a, 0x40, 0x58, 0x7d, 0xec, 0x38, 0x07, 0x2e, 0x17, 0x29, 0xb7, 0x4c, 0x79, 0x54,
|
||||
0x7c, 0xab, 0xb7, 0xa2, 0x1d, 0x82, 0xb6, 0x77, 0xa1, 0x6a, 0x32, 0x62, 0x47, 0xc4, 0xad, 0x24,
|
||||
0x89, 0x1b, 0x00, 0xd2, 0xc2, 0x4d, 0xea, 0x5f, 0x6a, 0x70, 0x96, 0x4b, 0xec, 0x91, 0xb0, 0xe9,
|
||||
0x0d, 0x4a, 0x6f, 0x13, 0x86, 0x4d, 0xcb, 0xff, 0x5e, 0x40, 0xbc, 0xfe, 0x6b, 0x56, 0x0c, 0x03,
|
||||
0x66, 0x42, 0x63, 0x92, 0x8e, 0xfe, 0xc4, 0x0b, 0x51, 0x09, 0x7e, 0x50, 0x7d, 0x96, 0x5f, 0x4f,
|
||||
0xf5, 0x99, 0x57, 0x0d, 0x56, 0xa6, 0x54, 0x0d, 0x8e, 0x6e, 0x08, 0x24, 0xda, 0x0c, 0x33, 0xe9,
|
||||
0x36, 0x43, 0x4e, 0xc4, 0x9a, 0x3d, 0x6e, 0x55, 0x54, 0xcb, 0xad, 0x8a, 0xec, 0x5c, 0x3b, 0xae,
|
||||
0x0b, 0x76, 0x7f, 0x37, 0xa9, 0x81, 0x23, 0x75, 0x6d, 0x92, 0xfa, 0x08, 0x5e, 0x6b, 0x7d, 0xf4,
|
||||
0x79, 0xaa, 0xde, 0x09, 0x1b, 0x18, 0xef, 0x1f, 0xef, 0x4e, 0x63, 0x2a, 0x9f, 0xff, 0xbb, 0xaa,
|
||||
0xe1, 0x97, 0x22, 0x59, 0xa4, 0xee, 0x80, 0x07, 0x71, 0xf6, 0xc1, 0xe3, 0x10, 0xcf, 0x03, 0xa4,
|
||||
0xd3, 0xe2, 0xdf, 0xe8, 0x2a, 0x54, 0x38, 0x93, 0x65, 0x36, 0x7f, 0x26, 0xc9, 0x4f, 0x2e, 0x89,
|
||||
0x0d, 0x4a, 0x1f, 0x51, 0xa2, 0x6b, 0x62, 0x13, 0xba, 0x05, 0xf5, 0x58, 0xf1, 0xa5, 0x65, 0x9d,
|
||||
0x4f, 0x9e, 0x88, 0xed, 0x24, 0x3a, 0x36, 0xd8, 0xce, 0xcf, 0x76, 0x4c, 0x8f, 0xe8, 0x22, 0xd7,
|
||||
0xad, 0x0e, 0x9f, 0xbd, 0x1d, 0x2d, 0xc6, 0x67, 0xe3, 0xed, 0xe8, 0x1a, 0xcc, 0x84, 0x1d, 0x1f,
|
||||
0x61, 0x41, 0x73, 0xd7, 0xcf, 0x0e, 0x3b, 0xd3, 0xe8, 0x94, 0xdc, 0xa8, 0xbe, 0x28, 0xc1, 0xdb,
|
||||
0x03, 0x85, 0x88, 0xac, 0x29, 0x2a, 0x37, 0xde, 0x7c, 0xc4, 0xcd, 0xe9, 0xfc, 0x94, 0xa7, 0xd1,
|
||||
0xf9, 0x51, 0xff, 0x58, 0x84, 0x8b, 0xc3, 0x8c, 0xd9, 0xec, 0x62, 0x8f, 0xc5, 0xfa, 0x32, 0x0d,
|
||||
0xe6, 0x44, 0x11, 0xb4, 0x34, 0x88, 0xa0, 0x29, 0x86, 0x95, 0xd3, 0x0c, 0x53, 0xff, 0x5c, 0x82,
|
||||
0xb9, 0x84, 0x46, 0xe6, 0x45, 0x60, 0x9e, 0xee, 0x0a, 0x43, 0x10, 0x25, 0xb2, 0x88, 0x32, 0x75,
|
||||
0x2d, 0x31, 0x83, 0x7a, 0x00, 0x14, 0x7b, 0xd8, 0x26, 0x8c, 0x78, 0x3c, 0x34, 0x70, 0x17, 0x72,
|
||||
0x6f, 0x72, 0x77, 0xb5, 0x17, 0xc1, 0xd4, 0x12, 0xe0, 0x79, 0xbe, 0x2e, 0x50, 0xfb, 0x32, 0x20,
|
||||
0xc8, 0x11, 0x7a, 0x06, 0x0b, 0x07, 0xa6, 0x45, 0xf6, 0x06, 0x84, 0xcc, 0x08, 0x42, 0x1e, 0x4e,
|
||||
0x4e, 0xc8, 0xdd, 0x24, 0x5c, 0x2d, 0x83, 0x46, 0xbd, 0x02, 0x8d, 0xac, 0x81, 0x72, 0x22, 0x4d,
|
||||
0x1b, 0x1b, 0x31, 0xb7, 0xe4, 0x48, 0x45, 0xd0, 0xc8, 0x1a, 0xa4, 0xfa, 0xcf, 0x12, 0x2c, 0xc7,
|
||||
0xe0, 0x36, 0x1c, 0xc7, 0x0d, 0x1c, 0x5d, 0x74, 0x65, 0x73, 0x65, 0x71, 0x1a, 0xaa, 0xcc, 0x64,
|
||||
0x56, 0x9c, 0x49, 0x89, 0x01, 0x0f, 0x86, 0xcc, 0x75, 0x2d, 0x66, 0x52, 0x29, 0xe0, 0x68, 0x18,
|
||||
0xca, 0xfe, 0x69, 0x60, 0x7a, 0xa4, 0x23, 0x5c, 0x4b, 0x4d, 0x8b, 0xc7, 0x7c, 0x8d, 0xa7, 0x49,
|
||||
0xa2, 0x88, 0x09, 0x99, 0x19, 0x8f, 0x79, 0x68, 0xd4, 0x5d, 0xcb, 0x22, 0x3a, 0x67, 0x47, 0xa2,
|
||||
0xcc, 0xc9, 0xcc, 0x8a, 0xf2, 0x89, 0x79, 0xa6, 0x63, 0xc8, 0x22, 0x47, 0x8e, 0x38, 0x9d, 0xd8,
|
||||
0xf3, 0x70, 0x5f, 0xd6, 0x36, 0xe1, 0x00, 0x7d, 0x04, 0x65, 0x1b, 0x53, 0x19, 0x39, 0xaf, 0xa4,
|
||||
0xdc, 0x4d, 0x1e, 0x07, 0x5a, 0xbb, 0x98, 0x86, 0xa1, 0x85, 0x1f, 0x6b, 0x7e, 0x00, 0xb5, 0x68,
|
||||
0xe2, 0x6b, 0xe5, 0x98, 0x5f, 0xc2, 0xa9, 0x94, 0x37, 0x43, 0x8f, 0x61, 0x65, 0xa0, 0x51, 0x49,
|
||||
0x84, 0x32, 0xab, 0x7c, 0xfb, 0x48, 0xca, 0xb4, 0x11, 0x00, 0xd4, 0xa7, 0xb0, 0xc4, 0x55, 0x46,
|
||||
0x18, 0xfe, 0x94, 0x6a, 0xa5, 0x0f, 0xa1, 0x1e, 0xa3, 0xcc, 0xd5, 0x99, 0x26, 0xd4, 0x0e, 0xa3,
|
||||
0x6e, 0x79, 0x58, 0x2c, 0xc5, 0x63, 0x75, 0x03, 0x50, 0x92, 0x5e, 0x19, 0xd2, 0xae, 0xa6, 0xb3,
|
||||
0xec, 0xe5, 0x6c, 0xfc, 0x12, 0xdb, 0xa3, 0x24, 0xfb, 0x0f, 0x65, 0x58, 0xdc, 0x32, 0x45, 0xbf,
|
||||
0x68, 0x4a, 0x4e, 0xee, 0x0a, 0x34, 0xfc, 0xa0, 0x6d, 0xbb, 0x9d, 0xc0, 0x22, 0x32, 0xcb, 0x90,
|
||||
0xa9, 0xc3, 0xd0, 0xfc, 0x38, 0xe7, 0xc7, 0x99, 0x45, 0x31, 0xeb, 0xca, 0xfa, 0x5e, 0x7c, 0xa3,
|
||||
0x8f, 0xe0, 0xec, 0x03, 0xf2, 0x4c, 0xde, 0x67, 0xcb, 0x72, 0xdb, 0x6d, 0xd3, 0x31, 0x22, 0x24,
|
||||
0x61, 0xe7, 0x63, 0xf4, 0x86, 0xbc, 0xdc, 0x73, 0x26, 0x3f, 0xf7, 0xcc, 0x89, 0x54, 0xb3, 0x53,
|
||||
0x89, 0x54, 0xbf, 0x28, 0x42, 0x63, 0x20, 0x2e, 0x29, 0xf0, 0x9b, 0xa1, 0x61, 0x86, 0xe2, 0xbe,
|
||||
0x98, 0x14, 0x77, 0x76, 0xeb, 0xab, 0xdb, 0xe4, 0x7c, 0xd2, 0x26, 0xbf, 0x2a, 0xc1, 0xf2, 0x96,
|
||||
0xc9, 0x22, 0x6f, 0x68, 0xfe, 0xaf, 0xa9, 0x4e, 0x8e, 0xa0, 0x2b, 0xc7, 0x16, 0x74, 0x75, 0x2a,
|
||||
0x82, 0x6e, 0xc1, 0x4a, 0x96, 0xc3, 0x52, 0xda, 0xa7, 0xa1, 0x4a, 0xc5, 0x53, 0x41, 0xd8, 0x52,
|
||||
0x09, 0x07, 0xea, 0xbf, 0x6b, 0x70, 0xe1, 0x73, 0xda, 0xc1, 0x2c, 0xee, 0xdf, 0xdd, 0x75, 0x3d,
|
||||
0xf1, 0x56, 0x30, 0x1d, 0xd1, 0x64, 0x9e, 0x87, 0x4b, 0x63, 0x9f, 0x87, 0xcb, 0x63, 0x9e, 0x87,
|
||||
0x2b, 0xc7, 0x7a, 0x1e, 0xae, 0x4e, 0xed, 0x79, 0x78, 0xb8, 0xcc, 0x9c, 0xc9, 0x2d, 0x33, 0x1f,
|
||||
0xa7, 0x4a, 0xb1, 0x59, 0x61, 0x8b, 0xdf, 0x49, 0xda, 0xe2, 0x58, 0xe9, 0x8c, 0x7d, 0x88, 0xca,
|
||||
0xbc, 0xaa, 0xd6, 0x8e, 0x7c, 0x55, 0xad, 0x0f, 0xbf, 0xaa, 0xe6, 0xbf, 0xa4, 0xc1, 0xc8, 0x97,
|
||||
0xb4, 0x4b, 0xb0, 0xe0, 0xf7, 0x1d, 0x9d, 0x74, 0xe2, 0xae, 0xee, 0x5c, 0x78, 0xed, 0xf4, 0x6c,
|
||||
0xca, 0xcc, 0xe6, 0x33, 0x66, 0x16, 0x6b, 0xea, 0xa9, 0x84, 0xa6, 0xe6, 0x19, 0xdf, 0xc2, 0xc8,
|
||||
0x0a, 0x3f, 0xf3, 0xc8, 0xb5, 0x98, 0xf7, 0xc8, 0x85, 0x7a, 0xd0, 0x88, 0xa8, 0x8a, 0x05, 0xd0,
|
||||
0x10, 0x02, 0xf8, 0xe4, 0xf8, 0x02, 0x78, 0x94, 0x81, 0x10, 0x8a, 0x61, 0x08, 0xf0, 0x7f, 0x4d,
|
||||
0x51, 0xdb, 0xfc, 0x75, 0x11, 0x96, 0x73, 0x89, 0x7e, 0x33, 0x35, 0xf6, 0x17, 0xb0, 0x3a, 0x8a,
|
||||
0xc1, 0xd2, 0x71, 0x29, 0x30, 0xab, 0x77, 0xb1, 0x63, 0x88, 0x6e, 0xb0, 0x68, 0xfa, 0xc8, 0xe1,
|
||||
0xb8, 0xa2, 0xf0, 0xfa, 0x8b, 0x79, 0x58, 0x1a, 0xd4, 0x66, 0xfc, 0xaf, 0xa9, 0x13, 0xf4, 0x10,
|
||||
0x1a, 0xd1, 0x83, 0x68, 0xf4, 0xa0, 0x80, 0xc6, 0xbd, 0xf6, 0x35, 0xcf, 0xe7, 0x2f, 0x86, 0xa4,
|
||||
0xa9, 0x05, 0xa4, 0xc3, 0xd9, 0x2c, 0xc0, 0xc1, 0xc3, 0xe2, 0xb7, 0xc6, 0x40, 0x8e, 0x77, 0x1d,
|
||||
0x85, 0xe2, 0x72, 0x11, 0x3d, 0x86, 0x85, 0xf4, 0xf3, 0x17, 0x4a, 0x25, 0xab, 0xb9, 0x2f, 0x72,
|
||||
0x4d, 0x75, 0xdc, 0x96, 0x98, 0xfe, 0x27, 0x5c, 0x2b, 0x53, 0xef, 0x37, 0x48, 0x4d, 0x37, 0x82,
|
||||
0xf2, 0xde, 0xca, 0x9a, 0xdf, 0x1c, 0xbb, 0x27, 0x86, 0xfe, 0x21, 0xd4, 0xa2, 0x27, 0x84, 0x34,
|
||||
0x9b, 0x33, 0x0f, 0x0b, 0xcd, 0x46, 0x1a, 0xde, 0x81, 0xaf, 0x16, 0xd0, 0xc7, 0x30, 0xc7, 0xb7,
|
||||
0x3d, 0xdc, 0xdc, 0xd9, 0xc7, 0xc6, 0x2b, 0x9d, 0xaf, 0x45, 0x2d, 0xf6, 0xe1, 0xc3, 0x89, 0xc6,
|
||||
0x7b, 0xf3, 0xad, 0x9c, 0x66, 0xb7, 0x5a, 0x40, 0x9f, 0x84, 0xf8, 0xf7, 0xe4, 0xef, 0x63, 0x56,
|
||||
0x5a, 0xe1, 0xcf, 0xb1, 0x5a, 0xd1, 0xcf, 0xb1, 0x5a, 0x77, 0x6c, 0xca, 0xfa, 0xcd, 0x9c, 0x6e,
|
||||
0xb4, 0x04, 0xf0, 0x04, 0x4e, 0x6d, 0x11, 0x36, 0x68, 0x1e, 0xa1, 0x8b, 0xc7, 0x6a, 0xb1, 0x35,
|
||||
0xd5, 0xec, 0xb6, 0xe1, 0xfe, 0x93, 0x5a, 0x40, 0x2f, 0x8a, 0xf0, 0xd6, 0x16, 0x61, 0xd9, 0x76,
|
||||
0x0c, 0x7a, 0x2f, 0x1f, 0xc9, 0x88, 0xb6, 0x4d, 0xf3, 0xc1, 0xa4, 0x36, 0x9d, 0x06, 0xab, 0x16,
|
||||
0xd0, 0x6f, 0x8b, 0xb0, 0xb0, 0x45, 0xb8, 0xdc, 0x62, 0x9a, 0xae, 0x8d, 0xa7, 0x29, 0xa7, 0x63,
|
||||
0xd2, 0x9c, 0xb0, 0xf5, 0x99, 0xc0, 0xae, 0x16, 0xd0, 0xef, 0x8a, 0x70, 0x26, 0xc1, 0xab, 0x24,
|
||||
0xbe, 0x57, 0xa1, 0xed, 0xb3, 0x09, 0x7f, 0x89, 0x95, 0x00, 0xa9, 0x16, 0xd0, 0x9e, 0x50, 0x93,
|
||||
0x41, 0x41, 0x86, 0x2e, 0xe4, 0x56, 0x5e, 0x31, 0xf6, 0xd5, 0x51, 0xcb, 0xb1, 0x6a, 0x7c, 0x06,
|
||||
0x73, 0x5b, 0x84, 0x45, 0x49, 0x7c, 0x5a, 0xf9, 0x33, 0x45, 0x5b, 0xda, 0xfb, 0x64, 0xf3, 0x7e,
|
||||
0xa1, 0xc4, 0x4b, 0x21, 0xac, 0x44, 0x4e, 0x99, 0x76, 0x3f, 0xb9, 0x19, 0x7d, 0x5a, 0x89, 0xf3,
|
||||
0x53, 0x52, 0xb5, 0x80, 0x9e, 0xc2, 0x4a, 0xbe, 0xf7, 0x47, 0xef, 0x1c, 0x3b, 0x04, 0x37, 0xaf,
|
||||
0x1c, 0x67, 0x6b, 0x84, 0xf2, 0xd3, 0x8d, 0xbf, 0xbe, 0x5c, 0x2d, 0xfe, 0xfd, 0xe5, 0x6a, 0xf1,
|
||||
0xab, 0x97, 0xab, 0xc5, 0x1f, 0xde, 0x38, 0xe2, 0x17, 0x9b, 0x89, 0x1f, 0x81, 0x62, 0x6a, 0xea,
|
||||
0x96, 0x49, 0x1c, 0xd6, 0x9e, 0x11, 0x2e, 0xe0, 0xc6, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x58,
|
||||
0x77, 0x20, 0x64, 0x23, 0x2a, 0x00, 0x00,
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
|
|
@ -3515,17 +3525,19 @@ func (m *ManifestRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|||
dAtA[i] = 0x8a
|
||||
}
|
||||
}
|
||||
if m.VerifySignature {
|
||||
i--
|
||||
if m.VerifySignature {
|
||||
dAtA[i] = 1
|
||||
} else {
|
||||
dAtA[i] = 0
|
||||
if m.SourceIntegrity != nil {
|
||||
{
|
||||
size, err := m.SourceIntegrity.MarshalToSizedBuffer(dAtA[:i])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i -= size
|
||||
i = encodeVarintRepository(dAtA, i, uint64(size))
|
||||
}
|
||||
i--
|
||||
dAtA[i] = 0x1
|
||||
i--
|
||||
dAtA[i] = 0x80
|
||||
dAtA[i] = 0x82
|
||||
}
|
||||
if len(m.ApiVersions) > 0 {
|
||||
for iNdEx := len(m.ApiVersions) - 1; iNdEx >= 0; iNdEx-- {
|
||||
|
|
@ -4034,6 +4046,18 @@ func (m *ManifestResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|||
i -= len(m.XXX_unrecognized)
|
||||
copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
if m.SourceIntegrityResult != nil {
|
||||
{
|
||||
size, err := m.SourceIntegrityResult.MarshalToSizedBuffer(dAtA[:i])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i -= size
|
||||
i = encodeVarintRepository(dAtA, i, uint64(size))
|
||||
}
|
||||
i--
|
||||
dAtA[i] = 0x4a
|
||||
}
|
||||
if len(m.Commands) > 0 {
|
||||
for iNdEx := len(m.Commands) - 1; iNdEx >= 0; iNdEx-- {
|
||||
i -= len(m.Commands[iNdEx])
|
||||
|
|
@ -4640,15 +4664,17 @@ func (m *RepoServerRevisionMetadataRequest) MarshalToSizedBuffer(dAtA []byte) (i
|
|||
i -= len(m.XXX_unrecognized)
|
||||
copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
if m.CheckSignature {
|
||||
i--
|
||||
if m.CheckSignature {
|
||||
dAtA[i] = 1
|
||||
} else {
|
||||
dAtA[i] = 0
|
||||
if m.SourceIntegrity != nil {
|
||||
{
|
||||
size, err := m.SourceIntegrity.MarshalToSizedBuffer(dAtA[:i])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i -= size
|
||||
i = encodeVarintRepository(dAtA, i, uint64(size))
|
||||
}
|
||||
i--
|
||||
dAtA[i] = 0x18
|
||||
dAtA[i] = 0x1a
|
||||
}
|
||||
if len(m.Revision) > 0 {
|
||||
i -= len(m.Revision)
|
||||
|
|
@ -5161,15 +5187,17 @@ func (m *GitFilesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|||
i -= len(m.XXX_unrecognized)
|
||||
copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
if m.VerifyCommit {
|
||||
i--
|
||||
if m.VerifyCommit {
|
||||
dAtA[i] = 1
|
||||
} else {
|
||||
dAtA[i] = 0
|
||||
if m.SourceIntegrity != nil {
|
||||
{
|
||||
size, err := m.SourceIntegrity.MarshalToSizedBuffer(dAtA[:i])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i -= size
|
||||
i = encodeVarintRepository(dAtA, i, uint64(size))
|
||||
}
|
||||
i--
|
||||
dAtA[i] = 0x38
|
||||
dAtA[i] = 0x3a
|
||||
}
|
||||
if m.NoRevisionCache {
|
||||
i--
|
||||
|
|
@ -5302,15 +5330,17 @@ func (m *GitDirectoriesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|||
i -= len(m.XXX_unrecognized)
|
||||
copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
if m.VerifyCommit {
|
||||
i--
|
||||
if m.VerifyCommit {
|
||||
dAtA[i] = 1
|
||||
} else {
|
||||
dAtA[i] = 0
|
||||
if m.SourceIntegrity != nil {
|
||||
{
|
||||
size, err := m.SourceIntegrity.MarshalToSizedBuffer(dAtA[:i])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i -= size
|
||||
i = encodeVarintRepository(dAtA, i, uint64(size))
|
||||
}
|
||||
i--
|
||||
dAtA[i] = 0x28
|
||||
dAtA[i] = 0x2a
|
||||
}
|
||||
if m.NoRevisionCache {
|
||||
i--
|
||||
|
|
@ -5703,8 +5733,9 @@ func (m *ManifestRequest) Size() (n int) {
|
|||
n += 1 + l + sovRepository(uint64(l))
|
||||
}
|
||||
}
|
||||
if m.VerifySignature {
|
||||
n += 3
|
||||
if m.SourceIntegrity != nil {
|
||||
l = m.SourceIntegrity.Size()
|
||||
n += 2 + l + sovRepository(uint64(l))
|
||||
}
|
||||
if len(m.HelmRepoCreds) > 0 {
|
||||
for _, e := range m.HelmRepoCreds {
|
||||
|
|
@ -5976,6 +6007,10 @@ func (m *ManifestResponse) Size() (n int) {
|
|||
n += 1 + l + sovRepository(uint64(l))
|
||||
}
|
||||
}
|
||||
if m.SourceIntegrityResult != nil {
|
||||
l = m.SourceIntegrityResult.Size()
|
||||
n += 1 + l + sovRepository(uint64(l))
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
|
|
@ -6219,8 +6254,9 @@ func (m *RepoServerRevisionMetadataRequest) Size() (n int) {
|
|||
if l > 0 {
|
||||
n += 1 + l + sovRepository(uint64(l))
|
||||
}
|
||||
if m.CheckSignature {
|
||||
n += 2
|
||||
if m.SourceIntegrity != nil {
|
||||
l = m.SourceIntegrity.Size()
|
||||
n += 1 + l + sovRepository(uint64(l))
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
|
|
@ -6474,8 +6510,9 @@ func (m *GitFilesRequest) Size() (n int) {
|
|||
if m.NoRevisionCache {
|
||||
n += 2
|
||||
}
|
||||
if m.VerifyCommit {
|
||||
n += 2
|
||||
if m.SourceIntegrity != nil {
|
||||
l = m.SourceIntegrity.Size()
|
||||
n += 1 + l + sovRepository(uint64(l))
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
|
|
@ -6527,8 +6564,9 @@ func (m *GitDirectoriesRequest) Size() (n int) {
|
|||
if m.NoRevisionCache {
|
||||
n += 2
|
||||
}
|
||||
if m.VerifyCommit {
|
||||
n += 2
|
||||
if m.SourceIntegrity != nil {
|
||||
l = m.SourceIntegrity.Size()
|
||||
n += 1 + l + sovRepository(uint64(l))
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
|
|
@ -7093,10 +7131,10 @@ func (m *ManifestRequest) Unmarshal(dAtA []byte) error {
|
|||
m.ApiVersions = append(m.ApiVersions, string(dAtA[iNdEx:postIndex]))
|
||||
iNdEx = postIndex
|
||||
case 16:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field VerifySignature", wireType)
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field SourceIntegrity", wireType)
|
||||
}
|
||||
var v int
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowRepository
|
||||
|
|
@ -7106,12 +7144,28 @@ func (m *ManifestRequest) Unmarshal(dAtA []byte) error {
|
|||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
v |= int(b&0x7F) << shift
|
||||
msglen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
m.VerifySignature = bool(v != 0)
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if m.SourceIntegrity == nil {
|
||||
m.SourceIntegrity = &v1alpha1.SourceIntegrity{}
|
||||
}
|
||||
if err := m.SourceIntegrity.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
case 17:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field HelmRepoCreds", wireType)
|
||||
|
|
@ -8711,6 +8765,42 @@ func (m *ManifestResponse) Unmarshal(dAtA []byte) error {
|
|||
}
|
||||
m.Commands = append(m.Commands, string(dAtA[iNdEx:postIndex]))
|
||||
iNdEx = postIndex
|
||||
case 9:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field SourceIntegrityResult", wireType)
|
||||
}
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowRepository
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
msglen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if m.SourceIntegrityResult == nil {
|
||||
m.SourceIntegrityResult = &v1alpha1.SourceIntegrityCheckResult{}
|
||||
}
|
||||
if err := m.SourceIntegrityResult.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipRepository(dAtA[iNdEx:])
|
||||
|
|
@ -10417,10 +10507,10 @@ func (m *RepoServerRevisionMetadataRequest) Unmarshal(dAtA []byte) error {
|
|||
m.Revision = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
case 3:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field CheckSignature", wireType)
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field SourceIntegrity", wireType)
|
||||
}
|
||||
var v int
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowRepository
|
||||
|
|
@ -10430,12 +10520,28 @@ func (m *RepoServerRevisionMetadataRequest) Unmarshal(dAtA []byte) error {
|
|||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
v |= int(b&0x7F) << shift
|
||||
msglen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
m.CheckSignature = bool(v != 0)
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if m.SourceIntegrity == nil {
|
||||
m.SourceIntegrity = &v1alpha1.SourceIntegrity{}
|
||||
}
|
||||
if err := m.SourceIntegrity.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipRepository(dAtA[iNdEx:])
|
||||
|
|
@ -11942,10 +12048,10 @@ func (m *GitFilesRequest) Unmarshal(dAtA []byte) error {
|
|||
}
|
||||
m.NoRevisionCache = bool(v != 0)
|
||||
case 7:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field VerifyCommit", wireType)
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field SourceIntegrity", wireType)
|
||||
}
|
||||
var v int
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowRepository
|
||||
|
|
@ -11955,12 +12061,28 @@ func (m *GitFilesRequest) Unmarshal(dAtA []byte) error {
|
|||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
v |= int(b&0x7F) << shift
|
||||
msglen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
m.VerifyCommit = bool(v != 0)
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if m.SourceIntegrity == nil {
|
||||
m.SourceIntegrity = &v1alpha1.SourceIntegrity{}
|
||||
}
|
||||
if err := m.SourceIntegrity.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipRepository(dAtA[iNdEx:])
|
||||
|
|
@ -12300,10 +12422,10 @@ func (m *GitDirectoriesRequest) Unmarshal(dAtA []byte) error {
|
|||
}
|
||||
m.NoRevisionCache = bool(v != 0)
|
||||
case 5:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field VerifyCommit", wireType)
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field SourceIntegrity", wireType)
|
||||
}
|
||||
var v int
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowRepository
|
||||
|
|
@ -12313,12 +12435,28 @@ func (m *GitDirectoriesRequest) Unmarshal(dAtA []byte) error {
|
|||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
v |= int(b&0x7F) << shift
|
||||
msglen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
m.VerifyCommit = bool(v != 0)
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if m.SourceIntegrity == nil {
|
||||
m.SourceIntegrity = &v1alpha1.SourceIntegrity{}
|
||||
}
|
||||
if err := m.SourceIntegrity.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipRepository(dAtA[iNdEx:])
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/fsnotify/fsnotify"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg"
|
||||
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
|
||||
)
|
||||
|
||||
const maxRecreateRetries = 5
|
||||
|
|
@ -61,9 +61,9 @@ func StartGPGWatcher(sourcePath string) error {
|
|||
// Force sync because we probably missed an event
|
||||
forceSync = true
|
||||
}
|
||||
if gpg.IsShortKeyID(path.Base(event.Name)) || forceSync {
|
||||
if sourceintegrity.IsShortKeyID(path.Base(event.Name)) || forceSync {
|
||||
log.Infof("Updating GPG keyring on filesystem event")
|
||||
added, removed, err := gpg.SyncKeyRingFromDirectory(sourcePath)
|
||||
added, removed, err := sourceintegrity.SyncKeyRingFromDirectory(sourcePath)
|
||||
if err != nil {
|
||||
log.Errorf("Could not sync keyring: %s", err.Error())
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import (
|
|||
gocache "github.com/patrickmn/go-cache"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/util/oci"
|
||||
|
||||
"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
|
||||
|
|
@ -59,7 +61,6 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/util/cmp"
|
||||
"github.com/argoproj/argo-cd/v3/util/git"
|
||||
"github.com/argoproj/argo-cd/v3/util/glob"
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg"
|
||||
"github.com/argoproj/argo-cd/v3/util/grpc"
|
||||
"github.com/argoproj/argo-cd/v3/util/helm"
|
||||
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
||||
|
|
@ -313,11 +314,13 @@ type operationContext struct {
|
|||
appPath string
|
||||
|
||||
// output of 'git verify-(tag/commit)', if signature verification is enabled (otherwise "")
|
||||
verificationResult string
|
||||
//
|
||||
// Deprecated: rely on sourceIntegrityResult. will be removed with the next major version.
|
||||
verificationResult string
|
||||
sourceIntegrityResult *v1alpha1.SourceIntegrityCheckResult
|
||||
}
|
||||
|
||||
// The 'operation' function parameter of 'runRepoOperation' may call this function to retrieve
|
||||
// the appPath or GPG verificationResult.
|
||||
// The 'operation' function parameter of 'runRepoOperation' may call this function to retrieve operationContext data.
|
||||
// Failure to generate either of these values will return an error which may be cached by
|
||||
// the calling function (for example, 'runManifestGen')
|
||||
type operationContextSrc = func() (*operationContext, error)
|
||||
|
|
@ -331,7 +334,7 @@ func (s *Service) runRepoOperation(
|
|||
revision string,
|
||||
repo *v1alpha1.Repository,
|
||||
source *v1alpha1.ApplicationSource,
|
||||
verifyCommit bool,
|
||||
sourceIntegrity *v1alpha1.SourceIntegrity,
|
||||
cacheFn func(cacheKey string, refSourceCommitSHAs cache.ResolvedRevisions, firstInvocation bool) (bool, error),
|
||||
operation func(repoRoot, commitSHA, cacheKey string, ctxSrc operationContextSrc) error,
|
||||
settings operationSettings,
|
||||
|
|
@ -423,7 +426,7 @@ func (s *Service) runRepoOperation(
|
|||
}
|
||||
|
||||
return operation(ociPath, revision, revision, func() (*operationContext, error) {
|
||||
return &operationContext{appPath, ""}, nil
|
||||
return &operationContext{appPath, "", nil}, nil
|
||||
})
|
||||
} else if source.IsHelm() {
|
||||
if settings.noCache {
|
||||
|
|
@ -458,7 +461,7 @@ func (s *Service) runRepoOperation(
|
|||
}
|
||||
}
|
||||
return operation(chartPath, revision, revision, func() (*operationContext, error) {
|
||||
return &operationContext{chartPath, ""}, nil
|
||||
return &operationContext{chartPath, "", nil}, nil
|
||||
})
|
||||
}
|
||||
closer, err := s.repoLock.Lock(gitClient.Root(), revision, settings.allowConcurrent, func(clean bool) (goio.Closer, error) {
|
||||
|
|
@ -508,27 +511,30 @@ func (s *Service) runRepoOperation(
|
|||
// Here commitSHA refers to the SHA of the actual commit, whereas revision refers to the branch/tag name etc
|
||||
// We use the commitSHA to generate manifests and store them in cache, and revision to retrieve them from cache
|
||||
return operation(gitClient.Root(), commitSHA, revision, func() (*operationContext, error) {
|
||||
var signature string
|
||||
if verifyCommit {
|
||||
// When the revision is an annotated tag, we need to pass the unresolved revision (i.e. the tag name)
|
||||
// to the verification routine. For everything else, we work with the SHA that the target revision is
|
||||
// pointing to (i.e. the resolved revision).
|
||||
var rev string
|
||||
if gitClient.IsAnnotatedTag(revision) {
|
||||
rev = unresolvedRevision
|
||||
} else {
|
||||
rev = revision
|
||||
}
|
||||
signature, err = gitClient.VerifyCommitSignature(rev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Pass in the originalRevision to have access to the eventual tag name. Use resolved revision only if the originalRevision is unspecified.
|
||||
var rev string
|
||||
if unresolvedRevision != "" {
|
||||
rev = unresolvedRevision
|
||||
} else {
|
||||
rev = revision
|
||||
}
|
||||
sourceIntegrityResult, _, err := sourceintegrity.VerifyGit(sourceIntegrity, gitClient, rev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Computed and passed to preserve API backwards compatibility only. Decisions are made based on SourceIntegrityResult.
|
||||
verificationResult, err := gitClient.VerifyCommitSignature(rev) // nolint:staticcheck
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appPath, err := apppathutil.Path(gitClient.Root(), source.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &operationContext{appPath, signature}, nil
|
||||
|
||||
return &operationContext{appPath, verificationResult, sourceIntegrityResult}, nil
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -667,7 +673,7 @@ func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestReq
|
|||
}
|
||||
|
||||
settings := operationSettings{sem: s.parallelismLimitSemaphore, noCache: q.NoCache, noRevisionCache: q.NoRevisionCache, allowConcurrent: q.ApplicationSource.AllowsConcurrentProcessing()}
|
||||
err = s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, q.VerifySignature, cacheFn, operation, settings, q.HasMultipleSources, q.RefSources)
|
||||
err = s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, q.SourceIntegrity, cacheFn, operation, settings, q.HasMultipleSources, q.RefSources)
|
||||
|
||||
// if the tarDoneCh message is sent it means that the manifest
|
||||
// generation is being managed by the cmp-server. In this case
|
||||
|
|
@ -729,7 +735,7 @@ func (s *Service) GenerateManifestWithFiles(stream apiclient.RepoServerService_G
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get app path: %w", err)
|
||||
}
|
||||
return &operationContext{appPath, ""}, nil
|
||||
return &operationContext{appPath, "", nil}, nil
|
||||
}, req)
|
||||
|
||||
var res *apiclient.ManifestResponse
|
||||
|
|
@ -975,6 +981,7 @@ func (s *Service) runManifestGenAsync(ctx context.Context, repoRoot, commitSHA,
|
|||
MostRecentError: "",
|
||||
}
|
||||
manifestGenResult.Revision = commitSHA
|
||||
manifestGenResult.SourceIntegrityResult = opContext.sourceIntegrityResult
|
||||
manifestGenResult.VerifyResult = opContext.verificationResult
|
||||
err = s.cache.SetManifests(cacheKey, appSourceCopy, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, &manifestGenCacheEntry, refSourceCommitSHAs, q.InstallationID)
|
||||
if err != nil {
|
||||
|
|
@ -2395,7 +2402,7 @@ func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppD
|
|||
}
|
||||
|
||||
settings := operationSettings{allowConcurrent: q.Source.AllowsConcurrentProcessing(), noCache: q.NoCache, noRevisionCache: q.NoCache || q.NoRevisionCache}
|
||||
err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, false, cacheFn, operation, settings, len(q.RefSources) > 0, q.RefSources)
|
||||
err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, nil, cacheFn, operation, settings, len(q.RefSources) > 0, q.RefSources)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
|
@ -2643,19 +2650,13 @@ func (s *Service) GetRevisionMetadata(_ context.Context, q *apiclient.RepoServer
|
|||
}
|
||||
metadata, err := s.cache.GetRevisionMetadata(q.Repo.Repo, q.Revision)
|
||||
if err == nil {
|
||||
// The logic here is that if a signature check on metadata is requested,
|
||||
// but there is none in the cache, we handle as if we have a cache miss
|
||||
// and re-generate the metadata. Otherwise, if there is signature info
|
||||
// in the metadata, but none was requested, we remove it from the data
|
||||
// that we return.
|
||||
if !q.CheckSignature || metadata.SignatureInfo != "" {
|
||||
// The SourceIntegrity criteria could have changed since this was cached - it could have been added, removed, or changed.
|
||||
// If present in request or the cached version, treat this as a cache miss.
|
||||
if q.SourceIntegrity == nil && metadata.SourceIntegrityResult == nil {
|
||||
log.Infof("revision metadata cache hit: %s/%s", q.Repo.Repo, q.Revision)
|
||||
if !q.CheckSignature {
|
||||
metadata.SignatureInfo = ""
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
log.Infof("revision metadata cache hit, but need to regenerate due to missing signature info: %s/%s", q.Repo.Repo, q.Revision)
|
||||
log.Infof("revision metadata cache hit, but need to regenerate due to source integrity checks: %s/%s", q.Repo.Repo, q.Revision)
|
||||
} else {
|
||||
if !errors.Is(err, cache.ErrCacheMiss) {
|
||||
log.Warnf("revision metadata cache error %s/%s: %v", q.Repo.Repo, q.Revision, err)
|
||||
|
|
@ -2681,30 +2682,14 @@ func (s *Service) GetRevisionMetadata(_ context.Context, q *apiclient.RepoServer
|
|||
|
||||
defer utilio.Close(closer)
|
||||
|
||||
m, err := gitClient.RevisionMetadata(q.Revision)
|
||||
sourceIntegrityResult, legacySignatureInfo, err := sourceintegrity.VerifyGit(q.SourceIntegrity, gitClient, q.Revision)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Run gpg verify-commit on the revision
|
||||
signatureInfo := ""
|
||||
if gpg.IsGPGEnabled() && q.CheckSignature {
|
||||
cs, err := gitClient.VerifyCommitSignature(q.Revision)
|
||||
if err != nil {
|
||||
log.Errorf("error verifying signature of commit '%s' in repo '%s': %v", q.Revision, q.Repo.Repo, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cs != "" {
|
||||
vr := gpg.ParseGitCommitVerification(cs)
|
||||
if vr.Result == gpg.VerifyResultUnknown {
|
||||
signatureInfo = "UNKNOWN signature: " + vr.Message
|
||||
} else {
|
||||
signatureInfo = fmt.Sprintf("%s signature from %s key %s", vr.Result, vr.Cipher, gpg.KeyID(vr.KeyID))
|
||||
}
|
||||
} else {
|
||||
signatureInfo = "Revision is not signed."
|
||||
}
|
||||
m, err := gitClient.RevisionMetadata(q.Revision)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
relatedRevisions := make([]v1alpha1.RevisionReference, len(m.References))
|
||||
|
|
@ -2724,7 +2709,16 @@ func (s *Service) GetRevisionMetadata(_ context.Context, q *apiclient.RepoServer
|
|||
},
|
||||
}
|
||||
}
|
||||
metadata = &v1alpha1.RevisionMetadata{Author: m.Author, Date: &metav1.Time{Time: m.Date}, Tags: m.Tags, Message: m.Message, SignatureInfo: signatureInfo, References: relatedRevisions}
|
||||
metadata = &v1alpha1.RevisionMetadata{
|
||||
Author: m.Author,
|
||||
Date: &metav1.Time{Time: m.Date},
|
||||
Tags: m.Tags,
|
||||
Message: m.Message,
|
||||
// TODO remove with next major version
|
||||
SignatureInfo: legacySignatureInfo,
|
||||
References: relatedRevisions,
|
||||
SourceIntegrityResult: sourceIntegrityResult,
|
||||
}
|
||||
_ = s.cache.SetRevisionMetadata(q.Repo.Repo, q.Revision, metadata)
|
||||
return metadata, nil
|
||||
}
|
||||
|
|
@ -3110,10 +3104,6 @@ func (s *Service) GetGitFiles(_ context.Context, request *apiclient.GitFilesRequ
|
|||
return nil, status.Errorf(codes.Internal, "unable to resolve git revision %s: %v", revision, err)
|
||||
}
|
||||
|
||||
if err := verifyCommitSignature(request.VerifyCommit, gitClient, revision, repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check the cache and return the results if present
|
||||
if cachedFiles, err := s.cache.GetGitFiles(repo.Repo, revision, gitPath); err == nil {
|
||||
log.Debugf("cache hit for repo: %s revision: %s pattern: %s", repo.Repo, revision, gitPath)
|
||||
|
|
@ -3134,6 +3124,14 @@ func (s *Service) GetGitFiles(_ context.Context, request *apiclient.GitFilesRequ
|
|||
}
|
||||
defer utilio.Close(closer)
|
||||
|
||||
sourceIntegrityResult, _, err := sourceintegrity.VerifyGit(request.SourceIntegrity, gitClient, revision)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sourceIntegrityResult.AsError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gitFiles, err := gitClient.LsFiles(gitPath, enableNewGitFileGlobbing)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "unable to list files. repo %s with revision %s pattern %s: %v", repo.Repo, revision, gitPath, err)
|
||||
|
|
@ -3159,26 +3157,6 @@ func (s *Service) GetGitFiles(_ context.Context, request *apiclient.GitFilesRequ
|
|||
}, nil
|
||||
}
|
||||
|
||||
func verifyCommitSignature(verifyCommit bool, gitClient git.Client, revision string, repo *v1alpha1.Repository) error {
|
||||
if gpg.IsGPGEnabled() && verifyCommit {
|
||||
cs, err := gitClient.VerifyCommitSignature(revision)
|
||||
if err != nil {
|
||||
log.Errorf("error verifying signature of commit '%s' in repo '%s': %v", revision, repo.Repo, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if cs == "" {
|
||||
return fmt.Errorf("revision %s is not signed", revision)
|
||||
}
|
||||
vr := gpg.ParseGitCommitVerification(cs)
|
||||
if vr.Result == gpg.VerifyResultUnknown {
|
||||
return fmt.Errorf("UNKNOWN signature: %s", vr.Message)
|
||||
}
|
||||
log.Debugf("%s signature from %s key %s", vr.Result, vr.Cipher, gpg.KeyID(vr.KeyID))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetGitDirectories(_ context.Context, request *apiclient.GitDirectoriesRequest) (*apiclient.GitDirectoriesResponse, error) {
|
||||
repo := request.GetRepo()
|
||||
revision := request.GetRevision()
|
||||
|
|
@ -3192,10 +3170,6 @@ func (s *Service) GetGitDirectories(_ context.Context, request *apiclient.GitDir
|
|||
return nil, status.Errorf(codes.Internal, "unable to resolve git revision %s: %v", revision, err)
|
||||
}
|
||||
|
||||
if err := verifyCommitSignature(request.VerifyCommit, gitClient, revision, repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check the cache and return the results if present
|
||||
if cachedPaths, err := s.cache.GetGitDirectories(repo.Repo, revision); err == nil {
|
||||
log.Debugf("cache hit for repo: %s revision: %s", repo.Repo, revision)
|
||||
|
|
@ -3216,6 +3190,14 @@ func (s *Service) GetGitDirectories(_ context.Context, request *apiclient.GitDir
|
|||
}
|
||||
defer utilio.Close(closer)
|
||||
|
||||
sourceIntegrityResult, _, err := sourceintegrity.VerifyGit(request.SourceIntegrity, gitClient, revision)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sourceIntegrityResult.AsError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoRoot := gitClient.Root()
|
||||
var paths []string
|
||||
if err := filepath.WalkDir(repoRoot, func(path string, entry fs.DirEntry, fnErr error) error {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ message ManifestRequest {
|
|||
string kubeVersion = 14;
|
||||
// ApiVersions is the list of API versions from the destination cluster, used for rendering Helm charts.
|
||||
repeated string apiVersions = 15;
|
||||
// Request to verify the signature when generating the manifests (only for Git repositories)
|
||||
bool verifySignature = 16;
|
||||
// Source integrity constrains to verify the sources before use
|
||||
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.SourceIntegrity sourceIntegrity = 16;
|
||||
repeated github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.RepoCreds helmRepoCreds = 17;
|
||||
bool noRevisionCache = 18;
|
||||
string trackingMethod = 19;
|
||||
|
|
@ -98,9 +98,11 @@ message ManifestResponse {
|
|||
string revision = 4;
|
||||
string sourceType = 6;
|
||||
// Raw response of git verify-commit operation (always the empty string for Helm)
|
||||
// Deprecated: Use SourceIntegrityResult for more detailed information. VerifyResult will be removed with the next major version.
|
||||
string verifyResult = 7;
|
||||
// Commands is the list of commands used to hydrate the manifests
|
||||
repeated string commands = 8;
|
||||
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.SourceIntegrityCheckResult sourceIntegrityResult = 9;
|
||||
}
|
||||
|
||||
message ListRefsRequest {
|
||||
|
|
@ -163,8 +165,7 @@ message RepoServerRevisionMetadataRequest {
|
|||
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.Repository repo = 1;
|
||||
// the revision within the repo
|
||||
string revision = 2;
|
||||
// whether to check signature on revision
|
||||
bool checkSignature = 3;
|
||||
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.SourceIntegrity sourceIntegrity = 3;
|
||||
}
|
||||
|
||||
message RepoServerRevisionChartDetailsRequest {
|
||||
|
|
@ -247,7 +248,7 @@ message GitFilesRequest {
|
|||
string path = 4;
|
||||
bool NewGitFileGlobbingEnabled = 5;
|
||||
bool noRevisionCache = 6;
|
||||
bool verifyCommit = 7;
|
||||
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.SourceIntegrity sourceIntegrity = 7;
|
||||
}
|
||||
|
||||
message GitFilesResponse {
|
||||
|
|
@ -260,7 +261,7 @@ message GitDirectoriesRequest {
|
|||
bool submoduleEnabled = 2;
|
||||
string revision = 3;
|
||||
bool noRevisionCache = 4;
|
||||
bool verifyCommit = 5;
|
||||
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.SourceIntegrity sourceIntegrity = 5;
|
||||
}
|
||||
|
||||
message GitDirectoriesResponse {
|
||||
|
|
@ -345,7 +346,7 @@ service RepoServerService {
|
|||
// Get the meta-data (author, date, tags, message) for a specific revision of the OCI image
|
||||
rpc GetOCIMetadata(RepoServerRevisionChartDetailsRequest) returns (github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.OCIMetadata) {
|
||||
}
|
||||
|
||||
|
||||
// Get the chart details (author, date, tags, message) for a specific revision of the repo
|
||||
rpc GetRevisionChartDetails(RepoServerRevisionChartDetailsRequest) returns (github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.ChartDetails) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,10 +55,55 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
)
|
||||
|
||||
const testSignature = `gpg: Signature made Wed Feb 26 23:22:34 2020 CET
|
||||
gpg: using RSA key 4AEE18F83AFDEB23
|
||||
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]
|
||||
`
|
||||
var sourceIntegrityReqStrict = &v1alpha1.SourceIntegrity{
|
||||
Git: &v1alpha1.SourceIntegrityGit{
|
||||
Policies: []*v1alpha1.SourceIntegrityGitPolicy{
|
||||
{
|
||||
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{{URL: "*"}},
|
||||
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
|
||||
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeStrict,
|
||||
Keys: []string{"f24e21389b25a3c1", "ffffffffff25a3c1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var LsSignaturesMockOk = func(_ string, _ bool) (info []git.RevisionSignatureInfo, err error) {
|
||||
return []git.RevisionSignatureInfo{
|
||||
{
|
||||
Revision: "d71589b8001a0bd78bb311cb03c9d129c6f91de1",
|
||||
VerificationResult: git.GPGVerificationResultGood,
|
||||
SignatureKeyID: "f24e21389b25a3c1",
|
||||
Date: "Fri Oct 31 14:42:39 2025 +0100",
|
||||
AuthorIdentity: "Jane Doe <jdoe@acme.com>",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var LsSignaturesMockGitError = func(_ string, _ bool) (info []git.RevisionSignatureInfo, err error) {
|
||||
return []git.RevisionSignatureInfo{{
|
||||
Revision: "171589b8001a0bd78bb311cb03c9d129c6f91de1",
|
||||
VerificationResult: git.GPGVerificationResultExpiredKey,
|
||||
SignatureKeyID: "EXPIRED",
|
||||
Date: "Fri Oct 31 14:42:39 2025 +0100",
|
||||
AuthorIdentity: "Late Fred <lfred@acme.com>",
|
||||
}, {
|
||||
Revision: "111589b8001a0bd78bb311cb03c9d129c6f91de1",
|
||||
VerificationResult: git.GPGVerificationResultUnsigned,
|
||||
SignatureKeyID: "",
|
||||
Date: "Fri Oct 31 14:42:39 2025 +0100",
|
||||
AuthorIdentity: "Unsigned <unsigned@acme.com>",
|
||||
}}, nil
|
||||
}
|
||||
|
||||
var sourceIntegrityResultGitError = &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
|
||||
Name: "GIT/GPG",
|
||||
Problems: []string{
|
||||
"Failed verifying revision 171589b8001a0bd78bb311cb03c9d129c6f91de1 by 'Late Fred <lfred@acme.com>': signed with expired key (key_id=EXPIRED)",
|
||||
"Failed verifying revision 111589b8001a0bd78bb311cb03c9d129c6f91de1 by 'Unsigned <unsigned@acme.com>': unsigned (key_id=)",
|
||||
},
|
||||
}}}
|
||||
|
||||
type clientFunc func(*gitmocks.Client, *helmmocks.Client, *ocimocks.Client, *iomocks.TempPaths)
|
||||
|
||||
|
|
@ -102,7 +147,7 @@ func newCacheMocksWithOpts(repoCacheExpiration, revisionCacheExpiration, revisio
|
|||
}
|
||||
}
|
||||
|
||||
func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *gitmocks.Client, *repoCacheMocks) {
|
||||
func newServiceWithMocks(t *testing.T, root string) (*Service, *gitmocks.Client, *repoCacheMocks) {
|
||||
t.Helper()
|
||||
root, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
|
|
@ -116,12 +161,9 @@ func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *git
|
|||
gitClient.EXPECT().LsRemote(mock.Anything).Return(mock.Anything, nil)
|
||||
gitClient.EXPECT().CommitSHA().Return(mock.Anything, nil)
|
||||
gitClient.EXPECT().Root().Return(root)
|
||||
gitClient.EXPECT().RepoURL().Return("https://fake.com/fake_group/fake_repo.git")
|
||||
gitClient.EXPECT().IsAnnotatedTag(mock.Anything).Return(false)
|
||||
if signed {
|
||||
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return(testSignature, nil)
|
||||
} else {
|
||||
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return("", nil)
|
||||
}
|
||||
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return("", nil)
|
||||
|
||||
chart := "my-chart"
|
||||
oobChart := "out-of-bounds-chart"
|
||||
|
|
@ -176,13 +218,7 @@ func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *git
|
|||
|
||||
func newService(t *testing.T, root string) *Service {
|
||||
t.Helper()
|
||||
service, _, _ := newServiceWithMocks(t, root, false)
|
||||
return service
|
||||
}
|
||||
|
||||
func newServiceWithSignature(t *testing.T, root string) *Service {
|
||||
t.Helper()
|
||||
service, _, _ := newServiceWithMocks(t, root, true)
|
||||
service, _, _ := newServiceWithMocks(t, root)
|
||||
return service
|
||||
}
|
||||
|
||||
|
|
@ -201,6 +237,8 @@ func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service {
|
|||
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.EXPECT().LsRemote(revision).Return(revision, revisionErr)
|
||||
gitClient.EXPECT().IsAnnotatedTag(revision).Return(revisionErr != nil)
|
||||
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return("", nil)
|
||||
gitClient.EXPECT().CommitSHA().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
||||
gitClient.EXPECT().Root().Return(root)
|
||||
paths.EXPECT().GetPath(mock.Anything).Return(root, nil)
|
||||
|
|
@ -377,7 +415,7 @@ func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateManifests_EmptyCache(t *testing.T) {
|
||||
service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base", false)
|
||||
service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base")
|
||||
|
||||
src := v1alpha1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
|
|
@ -525,7 +563,7 @@ func TestGenerateManifestsHelmWithRefs_CachedNoLsRemote(t *testing.T) {
|
|||
// ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0)
|
||||
func TestHelmManifestFromChartRepo(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
service, gitMocks, mockCache := newServiceWithMocks(t, root, false)
|
||||
service, gitMocks, mockCache := newServiceWithMocks(t, root)
|
||||
source := &v1alpha1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"}
|
||||
request := &apiclient.ManifestRequest{
|
||||
Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something",
|
||||
|
|
@ -680,7 +718,7 @@ func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateManifestsUseExactRevision(t *testing.T) {
|
||||
service, gitClient, _ := newServiceWithMocks(t, ".", false)
|
||||
service, gitClient, _ := newServiceWithMocks(t, ".")
|
||||
|
||||
src := v1alpha1.ApplicationSource{Path: "./testdata/recurse", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
|
||||
|
||||
|
|
@ -1771,9 +1809,10 @@ func TestGetHelmCharts(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetRevisionMetadata(t *testing.T) {
|
||||
service, gitClient, _ := newServiceWithMocks(t, "../..", false)
|
||||
service, gitClient, _ := newServiceWithMocks(t, "../..")
|
||||
now := time.Now()
|
||||
|
||||
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(LsSignaturesMockOk)
|
||||
gitClient.EXPECT().RevisionMetadata(mock.Anything).Return(&git.RevisionMetadata{
|
||||
Message: "test",
|
||||
Author: "author",
|
||||
|
|
@ -1796,9 +1835,9 @@ func TestGetRevisionMetadata(t *testing.T) {
|
|||
}, nil)
|
||||
|
||||
res, err := service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
|
||||
Repo: &v1alpha1.Repository{},
|
||||
Revision: "c0b400fc458875d925171398f9ba9eabd5529923",
|
||||
CheckSignature: true,
|
||||
Repo: &v1alpha1.Repository{},
|
||||
Revision: "c0b400fc458875d925171398f9ba9eabd5529923",
|
||||
SourceIntegrity: sourceIntegrityReqStrict,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1806,16 +1845,16 @@ func TestGetRevisionMetadata(t *testing.T) {
|
|||
assert.Equal(t, now, res.Date.Time)
|
||||
assert.Equal(t, "author", res.Author)
|
||||
assert.Equal(t, []string{"tag1", "tag2"}, res.Tags)
|
||||
assert.NotEmpty(t, res.SignatureInfo)
|
||||
assert.True(t, res.SourceIntegrityResult.IsValid())
|
||||
require.Len(t, res.References, 1)
|
||||
require.NotNil(t, res.References[0].Commit)
|
||||
assert.Equal(t, "test-sha", res.References[0].Commit.SHA)
|
||||
|
||||
// Check for truncated revision value
|
||||
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
|
||||
Repo: &v1alpha1.Repository{},
|
||||
Revision: "c0b400f",
|
||||
CheckSignature: true,
|
||||
Repo: &v1alpha1.Repository{},
|
||||
Revision: "c0b400f",
|
||||
SourceIntegrity: sourceIntegrityReqStrict,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1823,95 +1862,111 @@ func TestGetRevisionMetadata(t *testing.T) {
|
|||
assert.Equal(t, now, res.Date.Time)
|
||||
assert.Equal(t, "author", res.Author)
|
||||
assert.Equal(t, []string{"tag1", "tag2"}, res.Tags)
|
||||
assert.NotEmpty(t, res.SignatureInfo)
|
||||
assert.True(t, res.SourceIntegrityResult.IsValid())
|
||||
|
||||
// Cache hit - signature info should not be in result
|
||||
// Cache hit, but SourceIntegrity removed, will be recreated without SourceIntegrityResult
|
||||
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
|
||||
Repo: &v1alpha1.Repository{},
|
||||
Revision: "c0b400fc458875d925171398f9ba9eabd5529923",
|
||||
CheckSignature: false,
|
||||
Repo: &v1alpha1.Repository{},
|
||||
Revision: "c0b400fc458875d925171398f9ba9eabd5529923",
|
||||
SourceIntegrity: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res.SignatureInfo)
|
||||
assert.Nil(t, res.SourceIntegrityResult)
|
||||
|
||||
// Enforce cache miss - signature info should not be in result
|
||||
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
|
||||
Repo: &v1alpha1.Repository{},
|
||||
Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091",
|
||||
CheckSignature: false,
|
||||
Repo: &v1alpha1.Repository{},
|
||||
Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091",
|
||||
SourceIntegrity: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res.SignatureInfo)
|
||||
assert.Nil(t, res.SourceIntegrityResult)
|
||||
|
||||
// Cache hit on previous entry that did not have signature info
|
||||
// Cache miss on the previous entry that did not have signature info - recreated
|
||||
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
|
||||
Repo: &v1alpha1.Repository{},
|
||||
Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091",
|
||||
CheckSignature: true,
|
||||
Repo: &v1alpha1.Repository{},
|
||||
Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091",
|
||||
SourceIntegrity: sourceIntegrityReqStrict,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, res.SignatureInfo)
|
||||
assert.True(t, res.SourceIntegrityResult.IsValid())
|
||||
}
|
||||
|
||||
func TestGetSignatureVerificationResult(t *testing.T) {
|
||||
// Commit with signature and verification requested
|
||||
{
|
||||
service := newServiceWithSignature(t, "../../manifests/base")
|
||||
service, gitClient, _ := newServiceWithMocks(t, "../../manifests/base")
|
||||
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(LsSignaturesMockOk)
|
||||
|
||||
src := v1alpha1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: &v1alpha1.Repository{},
|
||||
ApplicationSource: &src,
|
||||
VerifySignature: true,
|
||||
SourceIntegrity: sourceIntegrityReqStrict,
|
||||
ProjectName: "something",
|
||||
ProjectSourceRepos: []string{"*"},
|
||||
}
|
||||
|
||||
res, err := service.GenerateManifest(t.Context(), &q)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testSignature, res.VerifyResult)
|
||||
assert.True(t, res.SourceIntegrityResult.IsValid())
|
||||
require.NoError(t, res.SourceIntegrityResult.AsError())
|
||||
}
|
||||
// Commit with signature and verification not requested
|
||||
{
|
||||
service := newServiceWithSignature(t, "../../manifests/base")
|
||||
service, gitClient, _ := newServiceWithMocks(t, "../../manifests/base")
|
||||
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(LsSignaturesMockOk)
|
||||
|
||||
src := v1alpha1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: &v1alpha1.Repository{}, ApplicationSource: &src, ProjectName: "something",
|
||||
Repo: &v1alpha1.Repository{},
|
||||
ApplicationSource: &src,
|
||||
SourceIntegrity: nil,
|
||||
ProjectName: "something",
|
||||
ProjectSourceRepos: []string{"*"},
|
||||
}
|
||||
|
||||
res, err := service.GenerateManifest(t.Context(), &q)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res.VerifyResult)
|
||||
assert.Nil(t, res.SourceIntegrityResult)
|
||||
gitClient.AssertNotCalled(t, "LsSignatures", mock.Anything, mock.Anything)
|
||||
}
|
||||
// Commit without signature and verification requested
|
||||
{
|
||||
service := newService(t, "../../manifests/base")
|
||||
service, gitClient, _ := newServiceWithMocks(t, "../../manifests/base")
|
||||
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(LsSignaturesMockGitError)
|
||||
|
||||
src := v1alpha1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: &v1alpha1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
|
||||
Repo: &v1alpha1.Repository{},
|
||||
ApplicationSource: &src,
|
||||
SourceIntegrity: sourceIntegrityReqStrict,
|
||||
ProjectName: "something",
|
||||
ProjectSourceRepos: []string{"*"},
|
||||
}
|
||||
|
||||
res, err := service.GenerateManifest(t.Context(), &q)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res.VerifyResult)
|
||||
assert.Equal(t, sourceIntegrityResultGitError, res.SourceIntegrityResult)
|
||||
require.Error(t, res.SourceIntegrityResult.AsError())
|
||||
}
|
||||
// Commit without signature and verification not requested
|
||||
{
|
||||
service := newService(t, "../../manifests/base")
|
||||
service, gitClient, _ := newServiceWithMocks(t, "../../manifests/base")
|
||||
|
||||
src := v1alpha1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: &v1alpha1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
|
||||
Repo: &v1alpha1.Repository{},
|
||||
ApplicationSource: &src,
|
||||
SourceIntegrity: nil,
|
||||
ProjectName: "something",
|
||||
ProjectSourceRepos: []string{"*"},
|
||||
}
|
||||
|
||||
res, err := service.GenerateManifest(t.Context(), &q)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res.VerifyResult)
|
||||
assert.Nil(t, res.SourceIntegrityResult)
|
||||
gitClient.AssertNotCalled(t, "LsSignatures", mock.Anything, mock.Anything)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4493,11 +4548,11 @@ func TestErrorGetGitDirectories(t *testing.T) {
|
|||
Revision: "sadfsadf",
|
||||
},
|
||||
}, want: nil, wantErr: assert.Error},
|
||||
{name: "ErrorVerifyCommit", fields: fields{service: func() *Service {
|
||||
{name: "ErrorListingSignatures", fields: fields{service: func() *Service {
|
||||
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.EXPECT().LsRemote(mock.Anything).Return("", errors.New("ah error"))
|
||||
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return("", fmt.Errorf("revision %s is not signed", "sadfsadf"))
|
||||
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).Return([]git.RevisionSignatureInfo{}, errors.New("the thing have exploded"))
|
||||
gitClient.EXPECT().Root().Return(root)
|
||||
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
|
||||
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
|
||||
|
|
@ -4509,7 +4564,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
|
|||
Repo: &v1alpha1.Repository{Repo: "not-a-valid-url"},
|
||||
SubmoduleEnabled: false,
|
||||
Revision: "sadfsadf",
|
||||
VerifyCommit: true,
|
||||
SourceIntegrity: sourceIntegrityReqStrict,
|
||||
},
|
||||
}, want: nil, wantErr: assert.Error},
|
||||
}
|
||||
|
|
@ -5396,55 +5451,6 @@ func TestGetRevisionChartDetails(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestVerifyCommitSignature(t *testing.T) {
|
||||
repo := &v1alpha1.Repository{
|
||||
Repo: "https://github.com/example/repo.git",
|
||||
}
|
||||
|
||||
t.Run("VerifyCommitSignature with valid signature", func(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
mockGitClient := &gitmocks.Client{}
|
||||
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
|
||||
Return(testSignature, nil)
|
||||
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("VerifyCommitSignature with invalid signature", func(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
mockGitClient := &gitmocks.Client{}
|
||||
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
|
||||
Return("", nil)
|
||||
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
|
||||
assert.EqualError(t, err, "revision abcd1234 is not signed")
|
||||
})
|
||||
|
||||
t.Run("VerifyCommitSignature with unknown signature", func(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
mockGitClient := &gitmocks.Client{}
|
||||
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
|
||||
Return("", errors.New("UNKNOWN signature: gpg: Unknown signature from ABCDEFGH"))
|
||||
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
|
||||
assert.EqualError(t, err, "UNKNOWN signature: gpg: Unknown signature from ABCDEFGH")
|
||||
})
|
||||
|
||||
t.Run("VerifyCommitSignature with error verifying signature", func(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
mockGitClient := &gitmocks.Client{}
|
||||
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
|
||||
Return("", errors.New("error verifying signature of commit 'abcd1234' in repo 'https://github.com/example/repo.git': failed to verify signature"))
|
||||
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
|
||||
assert.EqualError(t, err, "error verifying signature of commit 'abcd1234' in repo 'https://github.com/example/repo.git': failed to verify signature")
|
||||
})
|
||||
|
||||
t.Run("VerifyCommitSignature with signature verification disabled", func(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "false")
|
||||
mockGitClient := &gitmocks.Client{}
|
||||
err := verifyCommitSignature(false, mockGitClient, "abcd1234", repo)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_GenerateManifests_Commands(t *testing.T) {
|
||||
t.Run("helm", func(t *testing.T) {
|
||||
service := newService(t, "testdata/my-chart")
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"time"
|
||||
|
||||
cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
|
||||
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
|
||||
|
||||
kubecache "github.com/argoproj/argo-cd/gitops-engine/pkg/cache"
|
||||
"github.com/argoproj/argo-cd/gitops-engine/pkg/diff"
|
||||
|
|
@ -1647,9 +1648,9 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe
|
|||
}
|
||||
defer utilio.Close(conn)
|
||||
return repoClient.GetRevisionMetadata(ctx, &apiclient.RepoServerRevisionMetadataRequest{
|
||||
Repo: repo,
|
||||
Revision: q.GetRevision(),
|
||||
CheckSignature: len(proj.Spec.SignatureKeys) > 0,
|
||||
Repo: repo,
|
||||
Revision: q.GetRevision(),
|
||||
SourceIntegrity: proj.EffectiveSourceIntegrity(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -2096,9 +2097,8 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR
|
|||
return nil, status.Error(codes.FailedPrecondition, "sync with replace was disabled on the API Server level via the server configuration")
|
||||
}
|
||||
|
||||
// We cannot use local manifests if we're only allowed to sync to signed commits
|
||||
if syncReq.Manifests != nil && len(proj.Spec.SignatureKeys) > 0 {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "Cannot use local sync when signature keys are required.")
|
||||
if syncReq.Manifests != nil && sourceintegrity.HasCriteria(proj.EffectiveSourceIntegrity(), a.Spec.GetSources()...) {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "Cannot use local manifests when source integrity is enforced")
|
||||
}
|
||||
|
||||
resources := []v1alpha1.SyncOperationResource{}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import (
|
|||
gpgkeypkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/gpgkey"
|
||||
appsv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/db"
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg"
|
||||
"github.com/argoproj/argo-cd/v3/util/rbac"
|
||||
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
|
||||
)
|
||||
|
||||
// Server provides a service of type GPGKeyService
|
||||
|
|
@ -51,9 +51,9 @@ func (s *Server) Get(ctx context.Context, q *gpgkeypkg.GnuPGPublicKeyQuery) (*ap
|
|||
return nil, err
|
||||
}
|
||||
|
||||
keyID := gpg.KeyID(q.KeyID)
|
||||
if keyID == "" {
|
||||
return nil, errors.New("KeyID is malformed or empty")
|
||||
keyID, err := sourceintegrity.KeyID(q.KeyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys, err := s.db.ListConfiguredGPGPublicKeys(ctx)
|
||||
|
|
|
|||
|
|
@ -154,60 +154,6 @@ func TestNamespacedGetLogsAllowNS(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestNamespacedSyncToUnsignedCommit(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
GivenWithNamespace(t, fixture.AppNamespace()).
|
||||
SetTrackingMethod("annotation").
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing))
|
||||
}
|
||||
|
||||
func TestNamespacedSyncToSignedCommitWKK(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
Given(t).
|
||||
SetAppNamespace(fixture.AppNamespace()).
|
||||
SetTrackingMethod("annotation").
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing))
|
||||
}
|
||||
|
||||
func TestNamespacedSyncToSignedCommitKWKK(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
Given(t).
|
||||
SetAppNamespace(fixture.AppNamespace()).
|
||||
SetTrackingMethod("annotation").
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy))
|
||||
}
|
||||
|
||||
func TestNamespacedAppCreation(t *testing.T) {
|
||||
ctx := Given(t)
|
||||
ctx.
|
||||
|
|
|
|||
426
test/e2e/app_management_source_integrity_test.go
Normal file
426
test/e2e/app_management_source_integrity_test.go
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
package e2e
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/gitops-engine/pkg/health"
|
||||
. "github.com/argoproj/argo-cd/gitops-engine/pkg/sync/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
. "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/test/e2e/fixture"
|
||||
. "github.com/argoproj/argo-cd/v3/test/e2e/fixture/app"
|
||||
)
|
||||
|
||||
var projectWithNoKeys = AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}},
|
||||
SourceIntegrity: &SourceIntegrity{
|
||||
Git: &SourceIntegrityGit{
|
||||
Policies: []*SourceIntegrityGitPolicy{{
|
||||
Repos: []SourceIntegrityGitPolicyRepo{{URL: "*"}},
|
||||
GPG: &SourceIntegrityGitPolicyGPG{
|
||||
Keys: []string{}, // Verifying but permitting no keys
|
||||
Mode: "head",
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSyncToUnsignedCommit(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, " unsigned (key_id=)"))
|
||||
}
|
||||
|
||||
func TestSyncToSignedCommitWithoutKnownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "signed with key not in keyring (key_id="+fixture.GpgGoodKeyID+")"))
|
||||
}
|
||||
|
||||
func TestSyncToSignedCommitWithKnownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(NoConditions())
|
||||
}
|
||||
|
||||
func TestSyncToSignedCommitWithUnallowedKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
ProjectSpec(projectWithNoKeys).
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "signed with unallowed key (key_id="+fixture.GpgGoodKeyID+")"))
|
||||
}
|
||||
|
||||
func TestSyncToSignedBranchWithKnownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
Revision("master").
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(NoConditions())
|
||||
}
|
||||
|
||||
func TestSyncToSignedBranchWithUnknownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
Revision("master").
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "signed with key not in keyring (key_id="+fixture.GpgGoodKeyID+")"))
|
||||
}
|
||||
|
||||
func TestSyncToUnsignedBranch(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Revision("master").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddFile("test.yaml", "TestSyncToUnsignedBranch").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "unsigned (key_id=)"))
|
||||
}
|
||||
|
||||
func TestSyncToSignedTagWithKnownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Revision("signed-tag").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedTag("signed-tag").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(NoConditions())
|
||||
}
|
||||
|
||||
func TestSyncToSignedTagWithUnknownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Revision("signed-tag").
|
||||
Path(guestbookPath).
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedTag("signed-tag").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision signed-tag by ")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "signed with key not in keyring (key_id="+fixture.GpgGoodKeyID+")"))
|
||||
}
|
||||
|
||||
func TestSyncToUnsignedAnnotatedTag(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Revision("unsigned-tag").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
// Signed commit with an unsigned annotated tag will validate the tag signature
|
||||
AddSignedFile("test.yaml", "null").
|
||||
AddAnnotatedTag("unsigned-tag", "message goes here").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision unsigned-tag by ")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "unsigned (key_id=)"))
|
||||
}
|
||||
|
||||
func TestSyncToUnsignedSimpleTag(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Revision("unsigned-simple-tag").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
// Signed commit with an unsigned not-annotated tag will validate the commit, not the tag
|
||||
AddSignedFile("test.yaml", "null").
|
||||
AddTag("unsigned-simple-tag").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(NoConditions())
|
||||
}
|
||||
|
||||
func TestSyncToSignedAnnotatedTagWithUnallowedKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
ProjectSpec(projectWithNoKeys).
|
||||
Revision("v1.0").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddFile("test.yaml", "null").
|
||||
AddSignedTag("v1.0").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision v1.0")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "signed with unallowed key (key_id="+fixture.GpgGoodKeyID+")"))
|
||||
}
|
||||
|
||||
func TestSyncToTagBasedConstraint(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Revision("1.*").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
AddSignedTag("1.0").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(NoConditions())
|
||||
}
|
||||
|
||||
func TestNamespacedSyncToUnsignedCommit(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
GivenWithNamespace(t, fixture.AppNamespace()).
|
||||
SetTrackingMethod("annotation").
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision ")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "unsigned (key_id=)"))
|
||||
}
|
||||
|
||||
func TestNamespacedSyncToSignedCommitWithUnknownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
SetAppNamespace(fixture.AppNamespace()).
|
||||
SetTrackingMethod("annotation").
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision ")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "signed with key not in keyring (key_id="+fixture.GpgGoodKeyID+")"))
|
||||
}
|
||||
|
||||
func TestNamespacedSyncToSignedCommit(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
SetAppNamespace(fixture.AppNamespace()).
|
||||
SetTrackingMethod("annotation").
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(NoConditions())
|
||||
}
|
||||
|
||||
func TestLocalManifestRejectedWithSourceIntegrity(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
And(func(app *Application) {
|
||||
res, _ := fixture.RunCli("app", "manifests", app.Name)
|
||||
assert.Contains(t, res, "containerPort: 80")
|
||||
assert.Contains(t, res, "image: quay.io/argoprojlabs/argocd-e2e-container:0.2")
|
||||
}).
|
||||
Given().
|
||||
LocalPath(guestbookPathLocal).
|
||||
When().
|
||||
IgnoreErrors().
|
||||
Sync("--local-repo-root", ".").
|
||||
Then().
|
||||
Expect(ErrorRegex("", "Cannot use local manifests when source integrity is enforced"))
|
||||
}
|
||||
|
||||
func TestOCISourceIgnoredWithSourceIntegrity(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
fixture.EnsureCleanState(t)
|
||||
// No keys in keyring, no keys in project, OCI is not git, yet source integrity is defined.
|
||||
// Expecting some of that would cause visible failure if the source integrity should be applied
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
ProjectSpec(appProjectWithSourceIntegrity()).
|
||||
HTTPSInsecureRepoURLWithClientCertAdded().
|
||||
PushImageToOCIRegistry("testdata/guestbook", "1.0.0").
|
||||
OCIRepoAdded("my-oci-repo", "guestbook").
|
||||
OCIRegistry(fixture.OCIHostURL).
|
||||
OCIRegistryPath("guestbook").
|
||||
RepoURLType(fixture.RepoURLTypeOCI).
|
||||
Revision("1.0.0").
|
||||
Path(".").
|
||||
When().
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
// Verify local manifests are permitted - source integrity criteria for git should not apply for oci
|
||||
Given().
|
||||
LocalPath(guestbookPathLocal).
|
||||
When().
|
||||
DoNotIgnoreErrors().
|
||||
Sync("--local-repo-root", ".", "--force", "--prune")
|
||||
}
|
||||
|
|
@ -159,166 +159,6 @@ func TestGetLogsAllow(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSyncToUnsignedCommit(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing))
|
||||
}
|
||||
|
||||
func TestSyncToSignedCommitWithoutKnownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing))
|
||||
}
|
||||
|
||||
func TestSyncToSignedCommitWithKnownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy))
|
||||
}
|
||||
|
||||
func TestSyncToSignedBranchWithKnownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
Revision("master").
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy))
|
||||
}
|
||||
|
||||
func TestSyncToSignedBranchWithUnknownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
Revision("master").
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing))
|
||||
}
|
||||
|
||||
func TestSyncToUnsignedBranch(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Revision("master").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing))
|
||||
}
|
||||
|
||||
func TestSyncToSignedTagWithKnownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Revision("signed-tag").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedTag("signed-tag").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy))
|
||||
}
|
||||
|
||||
func TestSyncToSignedTagWithUnknownKey(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Revision("signed-tag").
|
||||
Path(guestbookPath).
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedTag("signed-tag").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing))
|
||||
}
|
||||
|
||||
func TestSyncToUnsignedTag(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Revision("unsigned-tag").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddTag("unsigned-tag").
|
||||
IgnoreErrors().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing))
|
||||
}
|
||||
|
||||
func TestAppCreation(t *testing.T) {
|
||||
ctx := Given(t)
|
||||
ctx.
|
||||
|
|
|
|||
157
test/e2e/app_multiple_sources_source_integrity_test.go
Normal file
157
test/e2e/app_multiple_sources_source_integrity_test.go
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
package e2e
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/gitops-engine/pkg/health"
|
||||
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync/common"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
. "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
. "github.com/argoproj/argo-cd/v3/test/e2e/fixture"
|
||||
. "github.com/argoproj/argo-cd/v3/test/e2e/fixture/app"
|
||||
)
|
||||
|
||||
var oneShotSync = func(app *Application) {
|
||||
app.Spec.SyncPolicy = &SyncPolicy{
|
||||
Automated: &SyncPolicyAutomated{SelfHeal: ptr.To(true)},
|
||||
Retry: &RetryStrategy{Limit: 0},
|
||||
}
|
||||
}
|
||||
|
||||
func appProjectWithSourceIntegrity(keys ...string) AppProjectSpec {
|
||||
if keys == nil {
|
||||
keys = []string{}
|
||||
}
|
||||
return AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}},
|
||||
SourceIntegrity: &SourceIntegrity{
|
||||
Git: &SourceIntegrityGit{
|
||||
Policies: []*SourceIntegrityGitPolicy{{
|
||||
Repos: []SourceIntegrityGitPolicyRepo{{URL: "*"}},
|
||||
GPG: &SourceIntegrityGitPolicyGPG{
|
||||
Keys: keys,
|
||||
Mode: SourceIntegrityGitPolicyGPGModeHead,
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiSourceSourceIntegrityAllFailed(t *testing.T) {
|
||||
SkipOnEnv(t, "GPG")
|
||||
EnsureCleanState(t)
|
||||
|
||||
sources := []ApplicationSource{{
|
||||
RepoURL: RepoURL(RepoURLTypeFile),
|
||||
Path: guestbookPath,
|
||||
Name: "uno",
|
||||
}, {
|
||||
RepoURL: RepoURL(RepoURLTypeFile),
|
||||
Path: "two-nice-pods",
|
||||
}}
|
||||
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
ProjectSpec(appProjectWithSourceIntegrity(GpgGoodKeyID)).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
Sources(sources).
|
||||
When().
|
||||
IgnoreErrors().
|
||||
CreateMultiSourceAppFromFile(oneShotSync).
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(common.OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: source uno: Failed verifying revision")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: source 2 of 2: Failed verifying revision")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "unsigned (key_id=)")).
|
||||
// Should start passing after project update
|
||||
Given().
|
||||
When().
|
||||
AddSignedFile("fake.yaml", "change"). // Needs a new commit to avoid using cached manifests
|
||||
IgnoreErrors().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(common.OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(NoConditions())
|
||||
}
|
||||
|
||||
func TestMultiSourceSourceIntegritySomeFailed(t *testing.T) {
|
||||
SkipOnEnv(t, "GPG")
|
||||
EnsureCleanState(t)
|
||||
|
||||
sources := []ApplicationSource{{
|
||||
RepoURL: RepoURL(RepoURLTypeFile),
|
||||
Path: guestbookPath,
|
||||
Name: "guestbook",
|
||||
}, {
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps",
|
||||
Path: "blue-green",
|
||||
TargetRevision: "53e28ff20cc530b9ada2173fbbd64d48338583ba", // picking a precise commit so tests have a known signature
|
||||
Name: "blue-green",
|
||||
}}
|
||||
message := "GIT/GPG: source blue-green: Failed verifying revision 53e28ff20cc530b9ada2173fbbd64d48338583ba by 'May Zhang <may_zhang@intuit.com>': signed with key not in keyring (key_id=4AEE18F83AFDEB23)"
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
ProjectSpec(appProjectWithSourceIntegrity(GpgGoodKeyID)).
|
||||
Sources(sources).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("fake.yaml", "").
|
||||
IgnoreErrors().
|
||||
CreateMultiSourceAppFromFile(oneShotSync).
|
||||
Then().
|
||||
Expect(OperationPhaseIs(common.OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing)).
|
||||
Expect(Condition(ApplicationConditionComparisonError, message))
|
||||
}
|
||||
|
||||
func TestMultiSourceSourceIntegrityAllValid(t *testing.T) {
|
||||
SkipOnEnv(t, "GPG")
|
||||
EnsureCleanState(t)
|
||||
|
||||
sources := []ApplicationSource{{
|
||||
RepoURL: RepoURL(RepoURLTypeFile),
|
||||
Path: guestbookPath,
|
||||
Name: "valid",
|
||||
}, {
|
||||
RepoURL: RepoURL(RepoURLTypeFile),
|
||||
Path: ".",
|
||||
Name: "also-valid",
|
||||
}}
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
ProjectSpec(appProjectWithSourceIntegrity(GpgGoodKeyID)).
|
||||
Sources(sources).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("fake.yaml", "").
|
||||
IgnoreErrors().
|
||||
CreateMultiSourceAppFromFile(oneShotSync).
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(common.OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy)).
|
||||
Expect(NoConditions()).
|
||||
// Should start failing after key removal
|
||||
Given().
|
||||
GPGPublicKeyRemoved().
|
||||
When().
|
||||
AddSignedFile("fake.yaml", "change"). // Needs a new commit to avoid using cached manifests
|
||||
IgnoreErrors().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: source valid: Failed verifying revision")).
|
||||
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: source also-valid: Failed verifying revision"))
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ func TestMultiSourceAppCreation(t *testing.T) {
|
|||
ctx.
|
||||
Sources(sources).
|
||||
When().
|
||||
CreateMultiSourceAppFromFile().
|
||||
CreateMultiSourceApp().
|
||||
Then().
|
||||
And(func(app *Application) {
|
||||
assert.Equal(t, ctx.GetName(), app.Name)
|
||||
|
|
@ -80,7 +80,7 @@ func TestMultiSourceAppWithHelmExternalValueFiles(t *testing.T) {
|
|||
ctx.
|
||||
Sources(sources).
|
||||
When().
|
||||
CreateMultiSourceAppFromFile().
|
||||
CreateMultiSourceApp().
|
||||
Then().
|
||||
And(func(app *Application) {
|
||||
assert.Equal(t, ctx.GetName(), app.Name)
|
||||
|
|
@ -132,7 +132,7 @@ func TestMultiSourceAppWithSourceOverride(t *testing.T) {
|
|||
ctx.
|
||||
Sources(sources).
|
||||
When().
|
||||
CreateMultiSourceAppFromFile().
|
||||
CreateMultiSourceApp().
|
||||
Then().
|
||||
And(func(app *Application) {
|
||||
assert.Equal(t, ctx.GetName(), app.Name)
|
||||
|
|
@ -186,7 +186,7 @@ func TestMultiSourceAppWithSourceName(t *testing.T) {
|
|||
ctx.
|
||||
Sources(sources).
|
||||
When().
|
||||
CreateMultiSourceAppFromFile().
|
||||
CreateMultiSourceApp().
|
||||
Then().
|
||||
And(func(app *Application) {
|
||||
assert.Equal(t, ctx.GetName(), app.Name)
|
||||
|
|
@ -248,7 +248,7 @@ func TestMultiSourceAppSetWithSourceName(t *testing.T) {
|
|||
ctx.
|
||||
Sources(sources).
|
||||
When().
|
||||
CreateMultiSourceAppFromFile().
|
||||
CreateMultiSourceApp().
|
||||
Then().
|
||||
And(func(app *Application) {
|
||||
assert.Equal(t, ctx.GetName(), app.Name)
|
||||
|
|
@ -271,7 +271,7 @@ func TestMultiSourceAppSetWithSourceName(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestMultiSourceApptErrorWhenSourceNameAndSourcePosition(t *testing.T) {
|
||||
func TestMultiSourceAppErrorWhenSourceNameAndSourcePosition(t *testing.T) {
|
||||
sources := []ApplicationSource{{
|
||||
RepoURL: RepoURL(RepoURLTypeFile),
|
||||
Path: guestbookPath,
|
||||
|
|
@ -285,7 +285,7 @@ func TestMultiSourceApptErrorWhenSourceNameAndSourcePosition(t *testing.T) {
|
|||
ctx.
|
||||
Sources(sources).
|
||||
When().
|
||||
CreateMultiSourceAppFromFile().
|
||||
CreateMultiSourceApp().
|
||||
Then().
|
||||
Expect(Event(EventReasonResourceCreated, "create")).
|
||||
And(func(_ *Application) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package e2e
|
|||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -245,27 +246,10 @@ func TestSimpleGitDirectoryGeneratorGoTemplate(t *testing.T) {
|
|||
|
||||
func TestSimpleGitDirectoryGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
expectedErrorMessage := `error generating params from git: error getting directories from repo: error retrieving Git Directories: rpc error: code = Unknown desc = permission denied`
|
||||
expectedConditionsParamsError := []v1alpha1.ApplicationSetCondition{
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionErrorOccurred,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusTrue,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
|
||||
},
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionParametersGenerated,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
},
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
},
|
||||
}
|
||||
fixture.EnsureCleanState(t)
|
||||
expectedErrorMessage := regexp.MustCompile(
|
||||
`error generating params from git: error getting directories from repo: error retrieving Git Directories: rpc error: code = Unknown desc = GIT/GPG: Failed verifying revision .* by '.*': unsigned \(key_id=\)`,
|
||||
)
|
||||
generateExpectedApp := func(name string) v1alpha1.Application {
|
||||
return v1alpha1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
|
|
@ -299,6 +283,8 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
|||
|
||||
Given(t).
|
||||
When().
|
||||
// Create an unsigned local commit not to rely on whatever is in the repo's HEAD
|
||||
AddFile("test.yaml", randStr(t)).
|
||||
// Create a GitGenerator-based ApplicationSet
|
||||
Create(v1alpha1.ApplicationSet{
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
|
|
@ -320,7 +306,7 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
|||
Generators: []v1alpha1.ApplicationSetGenerator{
|
||||
{
|
||||
Git: &v1alpha1.GitGenerator{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
RepoURL: fixture.RepoURL("file://"),
|
||||
Directories: []v1alpha1.GitDirectoryGeneratorItem{
|
||||
{
|
||||
Path: guestbookPath,
|
||||
|
|
@ -333,34 +319,34 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
|||
}).
|
||||
Then().Expect(ApplicationsDoNotExist(expectedApps)).
|
||||
// verify the ApplicationSet error status conditions were set correctly
|
||||
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionErrorOccurred,
|
||||
v1alpha1.ApplicationSetConditionStatusTrue,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
|
||||
)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionParametersGenerated,
|
||||
v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionResourcesUpToDate,
|
||||
v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
)).
|
||||
When().
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
}
|
||||
|
||||
func TestSimpleGitDirectoryGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
expectedErrorMessage := `error generating params from git: error getting directories from repo: error retrieving Git Directories: rpc error: code = Unknown desc = permission denied`
|
||||
expectedConditionsParamsError := []v1alpha1.ApplicationSetCondition{
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionErrorOccurred,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusTrue,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
|
||||
},
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionParametersGenerated,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
},
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
},
|
||||
}
|
||||
fixture.EnsureCleanState(t)
|
||||
expectedErrorMessage := regexp.MustCompile(
|
||||
`error generating params from git: error getting directories from repo: error retrieving Git Directories: rpc error: code = Unknown desc = GIT/GPG: Failed verifying revision .* by '.*': signed with key not in keyring \(key_id=` + fixture.GpgGoodKeyID + `\)`,
|
||||
)
|
||||
generateExpectedApp := func(name string) v1alpha1.Application {
|
||||
return v1alpha1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
|
|
@ -396,7 +382,7 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
|||
Given(t).
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
AddSignedFile("test.yaml", randStr(t)).IgnoreErrors().
|
||||
AddSignedFile("test.yaml", randStr(t)).
|
||||
IgnoreErrors().
|
||||
// Create a GitGenerator-based ApplicationSet
|
||||
Create(v1alpha1.ApplicationSet{
|
||||
|
|
@ -423,7 +409,7 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
|||
Generators: []v1alpha1.ApplicationSetGenerator{
|
||||
{
|
||||
Git: &v1alpha1.GitGenerator{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
RepoURL: fixture.RepoURL("file://"),
|
||||
Directories: []v1alpha1.GitDirectoryGeneratorItem{
|
||||
{
|
||||
Path: guestbookPath,
|
||||
|
|
@ -435,7 +421,24 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
|||
},
|
||||
}).Then().
|
||||
// verify the ApplicationSet error status conditions were set correctly
|
||||
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionErrorOccurred,
|
||||
v1alpha1.ApplicationSetConditionStatusTrue,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
|
||||
)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionParametersGenerated,
|
||||
v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionResourcesUpToDate,
|
||||
v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
)).
|
||||
Expect(ApplicationsDoNotExist(expectedApps)).
|
||||
When().
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
|
|
@ -549,27 +552,9 @@ func TestSimpleGitFilesGenerator(t *testing.T) {
|
|||
|
||||
func TestSimpleGitFilesGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
expectedErrorMessage := `error generating params from git: error retrieving Git files: rpc error: code = Unknown desc = permission denied`
|
||||
expectedConditionsParamsError := []v1alpha1.ApplicationSetCondition{
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionErrorOccurred,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusTrue,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
|
||||
},
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionParametersGenerated,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
},
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
},
|
||||
}
|
||||
fixture.EnsureCleanState(t)
|
||||
expectedErrorMessage := regexp.MustCompile(`error generating params from git: error retrieving Git files: rpc error: code = Unknown desc = GIT/GPG: Failed verifying revision .* by '.*': unsigned \(key_id=\)`)
|
||||
|
||||
project := "gpg"
|
||||
generateExpectedApp := func(name string) v1alpha1.Application {
|
||||
return v1alpha1.Application{
|
||||
|
|
@ -597,15 +582,21 @@ func TestSimpleGitFilesGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
expectedApps := []v1alpha1.Application{
|
||||
unexpectedApps := []v1alpha1.Application{
|
||||
generateExpectedApp("engineering-dev-guestbook"),
|
||||
generateExpectedApp("engineering-prod-guestbook"),
|
||||
}
|
||||
|
||||
Given(t).
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
// Create an unsigned local commit not to rely on whatever is in the repo's HEAD
|
||||
AddFile("test.yaml", randStr(t)).
|
||||
// Create a GitGenerator-based ApplicationSet
|
||||
Create(v1alpha1.ApplicationSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "simple-git-generator",
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
Template: v1alpha1.ApplicationSetTemplate{
|
||||
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{cluster.name}}-guestbook"},
|
||||
|
|
@ -625,7 +616,7 @@ func TestSimpleGitFilesGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
|||
Generators: []v1alpha1.ApplicationSetGenerator{
|
||||
{
|
||||
Git: &v1alpha1.GitGenerator{
|
||||
RepoURL: "https://github.com/argoproj/applicationset.git",
|
||||
RepoURL: fixture.RepoURL("file://"),
|
||||
Files: []v1alpha1.GitFileGeneratorItem{
|
||||
{
|
||||
Path: "examples/git-generator-files-discovery/cluster-config/**/config.json",
|
||||
|
|
@ -635,36 +626,37 @@ func TestSimpleGitFilesGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
}).Then().Expect(ApplicationsDoNotExist(expectedApps)).
|
||||
// verify the ApplicationSet error status conditions were set correctly
|
||||
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
|
||||
When().
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
}).
|
||||
Then().Expect(ApplicationsDoNotExist(unexpectedApps)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionErrorOccurred,
|
||||
v1alpha1.ApplicationSetConditionStatusTrue,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
|
||||
)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionParametersGenerated,
|
||||
v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionResourcesUpToDate,
|
||||
v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
)).
|
||||
When().Delete(metav1.DeletePropagationForeground).
|
||||
Then().Expect(ApplicationsDoNotExist(unexpectedApps))
|
||||
}
|
||||
|
||||
func TestSimpleGitFilesGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
||||
fixture.SkipOnEnv(t, "GPG")
|
||||
expectedErrorMessage := `error generating params from git: error retrieving Git files: rpc error: code = Unknown desc = permission denied`
|
||||
expectedConditionsParamsError := []v1alpha1.ApplicationSetCondition{
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionErrorOccurred,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusTrue,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
|
||||
},
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionParametersGenerated,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
},
|
||||
{
|
||||
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
|
||||
Status: v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
Message: expectedErrorMessage,
|
||||
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
},
|
||||
}
|
||||
fixture.EnsureCleanState(t)
|
||||
expectedErrorMessage := regexp.MustCompile(
|
||||
`error generating params from git: error retrieving Git files: rpc error: code = Unknown desc = GIT/GPG: Failed verifying revision .* by '.*': signed with key not in keyring \(key_id=` + fixture.GpgGoodKeyID + `\)`,
|
||||
)
|
||||
|
||||
project := "gpg"
|
||||
generateExpectedApp := func(name string) v1alpha1.Application {
|
||||
return v1alpha1.Application{
|
||||
|
|
@ -700,7 +692,7 @@ func TestSimpleGitFilesGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
|||
Given(t).
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
AddSignedFile("test.yaml", randStr(t)).IgnoreErrors().
|
||||
AddSignedFile("test.yaml", randStr(t)).
|
||||
IgnoreErrors().
|
||||
// Create a GitGenerator-based ApplicationSet
|
||||
Create(v1alpha1.ApplicationSet{
|
||||
|
|
@ -723,7 +715,7 @@ func TestSimpleGitFilesGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
|||
Generators: []v1alpha1.ApplicationSetGenerator{
|
||||
{
|
||||
Git: &v1alpha1.GitGenerator{
|
||||
RepoURL: "https://github.com/argoproj/applicationset.git",
|
||||
RepoURL: fixture.RepoURL("file://"),
|
||||
Files: []v1alpha1.GitFileGeneratorItem{
|
||||
{
|
||||
Path: "examples/git-generator-files-discovery/cluster-config/**/config.json",
|
||||
|
|
@ -735,7 +727,24 @@ func TestSimpleGitFilesGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
|||
},
|
||||
}).Then().
|
||||
// verify the ApplicationSet error status conditions were set correctly
|
||||
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionErrorOccurred,
|
||||
v1alpha1.ApplicationSetConditionStatusTrue,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
|
||||
)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionParametersGenerated,
|
||||
v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
)).
|
||||
Expect(ApplicationSetHasCondition(
|
||||
v1alpha1.ApplicationSetConditionResourcesUpToDate,
|
||||
v1alpha1.ApplicationSetConditionStatusFalse,
|
||||
expectedErrorMessage,
|
||||
v1alpha1.ApplicationSetReasonErrorOccurred,
|
||||
)).
|
||||
Expect(ApplicationsDoNotExist(expectedApps)).
|
||||
When().
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
|
|
|
|||
|
|
@ -183,8 +183,15 @@ func (a *Actions) CreateFromFile(handler func(app *v1alpha1.Application), flags
|
|||
return a
|
||||
}
|
||||
|
||||
func (a *Actions) CreateMultiSourceAppFromFile(flags ...string) *Actions {
|
||||
func (a *Actions) CreateMultiSourceApp(flags ...string) *Actions {
|
||||
a.context.T().Helper()
|
||||
|
||||
return a.CreateMultiSourceAppFromFile(func(_ *v1alpha1.Application) {}, flags...)
|
||||
}
|
||||
|
||||
func (a *Actions) CreateMultiSourceAppFromFile(handler func(app *v1alpha1.Application), flags ...string) *Actions {
|
||||
a.context.T().Helper()
|
||||
|
||||
app := &v1alpha1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: a.context.AppName(),
|
||||
|
|
@ -205,6 +212,8 @@ func (a *Actions) CreateMultiSourceAppFromFile(flags ...string) *Actions {
|
|||
},
|
||||
}
|
||||
|
||||
handler(app)
|
||||
|
||||
data := grpc.MustMarshal(app)
|
||||
tmpFile, err := os.CreateTemp("", "")
|
||||
require.NoError(a.context.T(), err)
|
||||
|
|
|
|||
|
|
@ -233,6 +233,19 @@ func (a *Actions) Create(appSet v1alpha1.ApplicationSet) *Actions {
|
|||
// AppSet name is not configurable and should always be unique, based on the context name
|
||||
appSet.Name = a.context.GetName()
|
||||
|
||||
// Tests running in short succession using the same dummy generator URL, can cause repo-server to pull revisions
|
||||
// from gitRefCache effectively using the repo from a previous test when used before the cache expiry. This prevents
|
||||
// using the reference cache on appset creation to avoid that.
|
||||
for _, generator := range appSet.Spec.Generators {
|
||||
if generator.Git != nil {
|
||||
if appSet.Annotations == nil {
|
||||
appSet.Annotations = map[string]string{}
|
||||
}
|
||||
appSet.Annotations[common.AnnotationApplicationSetRefresh] = "true"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
newResource, err := appSetClientSet.Create(context.Background(), utils.MustToUnstructured(&appSet), metav1.CreateOptions{})
|
||||
|
||||
if err == nil {
|
||||
|
|
@ -562,6 +575,12 @@ func (a *Actions) runCli(args ...string) {
|
|||
a.verifyAction()
|
||||
}
|
||||
|
||||
func (a *Actions) AddFile(fileName, fileContents string) *Actions {
|
||||
a.context.T().Helper()
|
||||
fixture.AddFile(a.context.T(), a.context.path+"/"+fileName, fileContents)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Actions) AddSignedFile(fileName, fileContents string) *Actions {
|
||||
a.context.T().Helper()
|
||||
fixture.AddSignedFile(a.context.T(), a.context.path+"/"+fileName, fileContents)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package applicationsets
|
|||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -119,6 +120,23 @@ func ApplicationSetHasConditions(expectedConditions []v1alpha1.ApplicationSetCon
|
|||
}
|
||||
}
|
||||
|
||||
func ApplicationSetHasCondition(expType v1alpha1.ApplicationSetConditionType, expStatus v1alpha1.ApplicationSetConditionStatus, expMessage *regexp.Regexp, expReason string) Expectation {
|
||||
return func(c *Consequences) (state, string) {
|
||||
foundApplicationSet := c.applicationSet(c.context.GetName())
|
||||
if foundApplicationSet == nil {
|
||||
return pending, fmt.Sprintf("application set '%s' not found", c.context.GetName())
|
||||
}
|
||||
got := foundApplicationSet.Status.Conditions
|
||||
message := fmt.Sprintf("condition {%s %s %s %s} in %v", expType, expMessage, expStatus, expReason, got)
|
||||
for _, condition := range got {
|
||||
if expType == condition.Type && expStatus == condition.Status && expReason == condition.Reason && expMessage.MatchString(condition.Message) {
|
||||
return succeeded, message
|
||||
}
|
||||
}
|
||||
return pending, message
|
||||
}
|
||||
}
|
||||
|
||||
// ApplicationsDoNotExist checks that each of the 'expectedApps' no longer exist in the namespace
|
||||
func ApplicationsDoNotExist(expectedApps []v1alpha1.Application) Expectation {
|
||||
return func(c *Consequences) (state, string) {
|
||||
|
|
|
|||
|
|
@ -536,7 +536,6 @@ func SetAccounts(accounts map[string][]string) error {
|
|||
func SetPermissions(permissions []ACL, username string, roleName string) error {
|
||||
return updateRBACConfigMap(func(cm *corev1.ConfigMap) error {
|
||||
var aclstr strings.Builder
|
||||
|
||||
for _, permission := range permissions {
|
||||
_, _ = fmt.Fprintf(&aclstr, "p, role:%s, %s, %s, %s, allow \n", roleName, permission.Resource, permission.Action, permission.Scope)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/util/errors"
|
||||
)
|
||||
|
||||
// Add GPG public key via API and create appropriate file where the ConfigMap mount would de it as well
|
||||
// AddGPGPublicKey adds public key via API and creates the appropriate file where the ConfigMap mount would do it as well
|
||||
func AddGPGPublicKey(t *testing.T) {
|
||||
t.Helper()
|
||||
keyPath, err := filepath.Abs("../fixture/gpg/" + fixture.GpgGoodKeyID)
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg"
|
||||
|
||||
argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/typed/application/v1alpha1"
|
||||
applicationsv1 "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
|
||||
|
|
@ -862,11 +860,6 @@ func verifyGenerateManifests(
|
|||
continue
|
||||
}
|
||||
|
||||
verifySignature := false
|
||||
if len(proj.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled() {
|
||||
verifySignature = true
|
||||
}
|
||||
|
||||
repos := helmRepos
|
||||
helmRepoCreds := repositoryCredentials
|
||||
// If the source is OCI, there is a potential for an OCI image to be a Helm chart and that said chart in
|
||||
|
|
@ -887,7 +880,6 @@ func verifyGenerateManifests(
|
|||
Proxy: repoRes.Proxy,
|
||||
NoProxy: repoRes.NoProxy,
|
||||
},
|
||||
VerifySignature: verifySignature,
|
||||
Repos: repos,
|
||||
Revision: source.TargetRevision,
|
||||
AppName: app.Name,
|
||||
|
|
@ -898,6 +890,7 @@ func verifyGenerateManifests(
|
|||
KubeVersion: kubeVersion,
|
||||
ApiVersions: apiVersions,
|
||||
HelmOptions: helmOptions,
|
||||
SourceIntegrity: proj.EffectiveSourceIntegrity(),
|
||||
HelmRepoCreds: helmRepoCreds,
|
||||
TrackingMethod: trackingMethod,
|
||||
EnabledSourceTypes: enableGenerateManifests,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
appsv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg"
|
||||
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
|
||||
)
|
||||
|
||||
// Validates a single GnuPG key and returns the key's ID
|
||||
|
|
@ -32,7 +32,7 @@ func validatePGPKey(keyData string) (*appsv1.GnuPGPublicKey, error) {
|
|||
}
|
||||
}()
|
||||
|
||||
parsed, err := gpg.ValidatePGPKeys(f.Name())
|
||||
parsed, err := sourceintegrity.ValidatePGPKeys(f.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -69,9 +69,9 @@ func (db *db) ListConfiguredGPGPublicKeys(_ context.Context) (map[string]*appsv1
|
|||
// This is not optimal, but the executil from argo-pkg does not support writing to
|
||||
// stdin of the forked process. So for now, we must live with that.
|
||||
for k, p := range keysCM.Data {
|
||||
expectedKeyID := gpg.KeyID(k)
|
||||
if expectedKeyID == "" {
|
||||
return nil, fmt.Errorf("found entry with key '%s' in ConfigMap, but this is not a valid PGP key ID", k)
|
||||
expectedKeyID, err := sourceintegrity.KeyID(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("found invalid entry with key '%s' in ConfigMap: %s", k, err.Error())
|
||||
}
|
||||
parsedKey, err := validatePGPKey(p)
|
||||
if err != nil {
|
||||
|
|
@ -91,7 +91,7 @@ func (db *db) AddGPGPublicKey(ctx context.Context, keyData string) (map[string]*
|
|||
result := make(map[string]*appsv1.GnuPGPublicKey)
|
||||
skipped := make([]string, 0)
|
||||
|
||||
keys, err := gpg.ValidatePGPKeysFromString(keyData)
|
||||
keys, err := sourceintegrity.ValidatePGPKeysFromString(keyData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import (
|
|||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg/testdata"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
"github.com/argoproj/argo-cd/v3/util/sourceintegrity/testdata"
|
||||
)
|
||||
|
||||
// GPG config map with a single key and good mapping
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import (
|
|||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
|
|
@ -124,6 +126,7 @@ type gitRefCache interface {
|
|||
// Client is a generic git client interface
|
||||
type Client interface {
|
||||
Root() string
|
||||
RepoURL() string
|
||||
Init() error
|
||||
Fetch(revision string, depth int64) error
|
||||
Submodule() error
|
||||
|
|
@ -134,8 +137,15 @@ type Client interface {
|
|||
LsLargeFiles() ([]string, error)
|
||||
CommitSHA() (string, error)
|
||||
RevisionMetadata(revision string) (*RevisionMetadata, error)
|
||||
// Deprecated: To be removed in the next major version when Signature verification is replaced with Source Integrity.
|
||||
VerifyCommitSignature(string) (string, error)
|
||||
IsAnnotatedTag(string) bool
|
||||
// IsAnnotatedTag determines if the revision is, or resolves to an annotated tag.
|
||||
IsAnnotatedTag(revision string) bool
|
||||
// LsSignatures gets a list of revisions including their GPG signature info.
|
||||
// If revision is an annotated tag or a semantic constraint matching an annotated tag, its signature is reported as well
|
||||
// If deep==true, list the commits backwards in history until a signed "seal commit" or repo init commit. The listing includes those seal commits.
|
||||
// If deep==false, examines the revision only. Checking the annotated tag signature if the revision is an annotated tag, commit signature otherwise.
|
||||
LsSignatures(revision string, deep bool) ([]RevisionSignatureInfo, error)
|
||||
ChangedFiles(revision string, targetRevision string) ([]string, error)
|
||||
IsRevisionPresent(revision string) bool
|
||||
// SetAuthor sets the author name and email in the git configuration.
|
||||
|
|
@ -430,6 +440,10 @@ func (m *nativeGitClient) Root() string {
|
|||
return m.root
|
||||
}
|
||||
|
||||
func (m *nativeGitClient) RepoURL() string {
|
||||
return m.repoURL
|
||||
}
|
||||
|
||||
// Init initializes a local git repository and sets the remote origin
|
||||
func (m *nativeGitClient) Init() error {
|
||||
_, err := git.PlainOpen(m.root)
|
||||
|
|
@ -967,8 +981,11 @@ func updateCommitMetadata(logCtx *log.Entry, relatedCommit *CommitMetadata, line
|
|||
}
|
||||
|
||||
// VerifyCommitSignature Runs verify-commit on a given revision and returns the output
|
||||
//
|
||||
// Deprecated: To be removed in the next major version when Signature verification is replaced with Source Integrity.
|
||||
func (m *nativeGitClient) VerifyCommitSignature(revision string) (string, error) {
|
||||
out, err := m.runGnuPGWrapper(context.Background(), "git-verify-wrapper.sh", revision)
|
||||
cmd := m.cmdWithGPG(context.Background(), "git-verify-wrapper.sh", revision)
|
||||
out, err := m.runCmdOutput(cmd, runOpts{})
|
||||
if err != nil {
|
||||
log.Errorf("error verifying commit signature: %v", err)
|
||||
return "", errors.New("permission denied")
|
||||
|
|
@ -976,11 +993,314 @@ func (m *nativeGitClient) VerifyCommitSignature(revision string) (string, error)
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// IsAnnotatedTag returns true if the revision points to an annotated tag
|
||||
type (
|
||||
GPGVerificationResult string
|
||||
RevisionSignatureInfo struct {
|
||||
Revision string
|
||||
VerificationResult GPGVerificationResult
|
||||
SignatureKeyID string
|
||||
Date string
|
||||
AuthorIdentity string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
GPGVerificationResultGood GPGVerificationResult = "signed" // All good
|
||||
GPGVerificationResultBad GPGVerificationResult = "bad signature" // Not able to cryptographically verify signature
|
||||
GPGVerificationResultUntrusted GPGVerificationResult = "signed with untrusted key" // The trust level of the key in the gpg keyring is not sufficient
|
||||
GPGVerificationResultExpiredSignature GPGVerificationResult = "expired signature" // Signature have expired
|
||||
GPGVerificationResultExpiredKey GPGVerificationResult = "signed with expired key" // Signed with a key expired at the time of the signing
|
||||
GPGVerificationResultRevokedKey GPGVerificationResult = "signed with revoked key" // Signed with a key that is revoked
|
||||
GPGVerificationResultMissingKey GPGVerificationResult = "signed with key not in keyring" // The key used to sign was not added to the gpg keyring
|
||||
GPGVerificationResultUnsigned GPGVerificationResult = "unsigned" // Commit it not signed at all
|
||||
)
|
||||
|
||||
func gpgVerificationFromGpgCode(gpgCode string) GPGVerificationResult {
|
||||
// GPG code presented by `git verify-tag --raw`
|
||||
// https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes
|
||||
switch gpgCode {
|
||||
case "GOODSIG":
|
||||
return GPGVerificationResultGood
|
||||
case "BADSIG":
|
||||
return GPGVerificationResultBad
|
||||
case "EXPSIG":
|
||||
return GPGVerificationResultExpiredSignature
|
||||
case "EXPKEYSIG":
|
||||
return GPGVerificationResultExpiredKey
|
||||
case "REVKEYSIG":
|
||||
return GPGVerificationResultRevokedKey
|
||||
case "ERRSIG":
|
||||
return GPGVerificationResultMissingKey
|
||||
default:
|
||||
panic(fmt.Sprintf("Unable to parse VerificationResult from '%s'", gpgCode))
|
||||
}
|
||||
}
|
||||
|
||||
func gpgVerificationFromGitRevParse(oneLetter string) GPGVerificationResult {
|
||||
// The letters each represent a given verification result, as output by git rev-parse pretty format.
|
||||
// See PRETTY FORMAT in git-rev-list(1) for more information.
|
||||
// https://github.com/git/git/blob/5e6e4854e086ba0025bc7dc11e6b475c92a2f556/gpg-interface.c#L188
|
||||
switch oneLetter {
|
||||
case "G":
|
||||
return GPGVerificationResultGood
|
||||
case "B":
|
||||
return GPGVerificationResultBad
|
||||
case "U":
|
||||
return GPGVerificationResultUntrusted
|
||||
case "X":
|
||||
return GPGVerificationResultExpiredSignature
|
||||
case "Y":
|
||||
return GPGVerificationResultExpiredKey
|
||||
case "R":
|
||||
return GPGVerificationResultRevokedKey
|
||||
case "E":
|
||||
return GPGVerificationResultMissingKey
|
||||
case "N":
|
||||
return GPGVerificationResultUnsigned
|
||||
default:
|
||||
panic(fmt.Sprintf("Unable to parse VerificationResult from '%s'", oneLetter))
|
||||
}
|
||||
}
|
||||
|
||||
var gpgKeyIdRegexp = regexp.MustCompile("[0-9a-zA-Z]{16}")
|
||||
|
||||
func (m *nativeGitClient) tagSignature(tagRevision string) (*RevisionSignatureInfo, error) {
|
||||
ctx := context.Background()
|
||||
// Unlike for commits, there is no elegant way to slurp all signature info for tag. So this extracts details needed
|
||||
// for RevisionSignatureInfo from 2 different git invocations.
|
||||
cmd := m.cmdWithGPG(ctx, "git", "for-each-ref", "refs/tags/"+tagRevision, `--format=%(taggerdate),%(taggername) "%(taggeremail)"`)
|
||||
tagOut, err := m.runCmdOutput(cmd, runOpts{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tagOut == "" {
|
||||
return nil, fmt.Errorf("no tag found: %q", tagRevision)
|
||||
}
|
||||
tagInfo := strings.Split(tagOut, ",")
|
||||
if len(tagInfo) != 2 {
|
||||
return nil, fmt.Errorf("failed to parse tag %q for revisions %q", tagOut, tagRevision)
|
||||
}
|
||||
|
||||
cmd = m.cmdWithGPG(ctx, "git", "verify-tag", tagRevision, "--raw")
|
||||
tagGpgOut, err := m.runCmdOutput(cmd, runOpts{
|
||||
CaptureStderr: true, // The structured --raw output is printed to stderr only
|
||||
SkipErrorLogging: true, // Unsigned returns rc=1
|
||||
})
|
||||
status, keyId, err := evaluateGpgSignStatus(err, tagGpgOut)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gpg failed verifying git tag %q: %s", tagRevision, err.Error())
|
||||
}
|
||||
info, err := newRevisionSignatureInfo(tagRevision, status, keyId, tagInfo[0], tagInfo[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed building revision gpg signature info for tag %q: %s", tagRevision, err.Error())
|
||||
}
|
||||
return info, err
|
||||
}
|
||||
|
||||
func evaluateGpgSignStatus(cmdErr error, tagGpgOut string) (result GPGVerificationResult, keyId string, err error) {
|
||||
if cmdErr != nil {
|
||||
// Commit is not signed
|
||||
if tagGpgOut == "error: no signature found" {
|
||||
return GPGVerificationResultUnsigned, "", nil
|
||||
}
|
||||
|
||||
// Parse the output to extract info, ERRSIG causes `rc!=0`
|
||||
if !strings.Contains(tagGpgOut, "[GNUPG:] ERRSIG ") {
|
||||
return "", "", cmdErr
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes
|
||||
re := regexp.MustCompile(`\[GNUPG:] (GOODSIG|BADSIG|EXPSIG|EXPKEYSIG|REVKEYSIG|ERRSIG) ([0-9A-F]+) `)
|
||||
for line := range strings.Lines(tagGpgOut) {
|
||||
match := re.FindAllStringSubmatch(line, -1)
|
||||
switch len(match) {
|
||||
case 0:
|
||||
continue
|
||||
case 1:
|
||||
return gpgVerificationFromGpgCode(match[0][1]), match[0][2], nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("too many matches parsing line %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("unexpected `git verify-tag --raw` output: %q", tagGpgOut)
|
||||
}
|
||||
|
||||
func (m *nativeGitClient) LsSignatures(unresolvedRevision string, deep bool) ([]RevisionSignatureInfo, error) {
|
||||
// Resolve eventual semantic tag constraint before annotated tag detection
|
||||
if versions.IsConstraint(unresolvedRevision) {
|
||||
refs, err := m.getRefs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unresolvedRevision, err = versions.MaxVersion(unresolvedRevision, getGitTags(refs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var signatures []RevisionSignatureInfo
|
||||
if m.IsAnnotatedTag(unresolvedRevision) {
|
||||
signature, err := m.tagSignature(unresolvedRevision)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signatures = append(signatures, *signature)
|
||||
|
||||
// Check just the annotated tag
|
||||
if !deep {
|
||||
return signatures, nil
|
||||
}
|
||||
}
|
||||
|
||||
commitSignaturesRawOut, err := m.listRawSignatures(deep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Final LF will be cut by executil
|
||||
csvR := csv.NewReader(strings.NewReader(commitSignaturesRawOut))
|
||||
for {
|
||||
r, err := csvR.Read()
|
||||
// EOF means parsing had ended
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(r) < 5 {
|
||||
return nil, fmt.Errorf("invalid rev-list output for %q (fields=%d)", unresolvedRevision, len(r))
|
||||
}
|
||||
|
||||
revision := r[0]
|
||||
signatureInfo, err := newRevisionSignatureInfo(revision, gpgVerificationFromGitRevParse(r[1]), r[2], r[3], r[4])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed building revision gpg signature info for %q at %q: %s", unresolvedRevision, revision, err.Error())
|
||||
}
|
||||
signatures = append(signatures, *signatureInfo)
|
||||
}
|
||||
|
||||
return signatures, nil
|
||||
}
|
||||
|
||||
// newRevisionSignatureInfo builds valid RevisionSignatureInfo
|
||||
func newRevisionSignatureInfo(revision string, verificationResult GPGVerificationResult, signatureKeyID string, date string, authorIdentity string) (*RevisionSignatureInfo, error) {
|
||||
if revision == "" {
|
||||
return nil, errors.New("no revision specified")
|
||||
}
|
||||
if date == "" {
|
||||
return nil, errors.New("no date specified")
|
||||
}
|
||||
if authorIdentity == "" {
|
||||
return nil, errors.New("no author specified")
|
||||
}
|
||||
// Unsigned have no key ID, other states must have key ID
|
||||
if verificationResult == GPGVerificationResultUnsigned {
|
||||
if signatureKeyID != "" {
|
||||
return nil, fmt.Errorf("a gpg signing key id %q specified for unsigned commit", signatureKeyID)
|
||||
}
|
||||
} else {
|
||||
if !gpgKeyIdRegexp.MatchString(signatureKeyID) {
|
||||
return nil, fmt.Errorf("invalid gpg signing key %q", signatureKeyID)
|
||||
}
|
||||
}
|
||||
|
||||
return &RevisionSignatureInfo{
|
||||
Revision: revision,
|
||||
VerificationResult: verificationResult,
|
||||
SignatureKeyID: signatureKeyID,
|
||||
Date: date,
|
||||
AuthorIdentity: authorIdentity,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *nativeGitClient) listRawSignatures(deep bool) (string, error) {
|
||||
revisionSha, err := m.CommitSHA()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// This is using a two-step approach to solve the following problem: find all ancestors of a given revision in git history DAG,
|
||||
// stopping on a signed seal commit, or an init commit. Note there might be multiple seal commits that separate the revision
|
||||
// form init commit in case the history merged past the most recent seal commit.
|
||||
//
|
||||
// 1) Find all seal commits based on the trailer in their message. This searches the entire git history, which is unnecessary,
|
||||
// but there does not seem to be a decent way to stop on the most recent seal commits in each branch with a single git invocation.
|
||||
// Found commits are later eliminated to the correctly signed and trusted ones - this is to make sure that unsigned
|
||||
// or untrusted commits with a seal trailer do not stop the history verification.
|
||||
// 2) Find all the ancestor commits from the given revision stopping on any of the identified seal commits.
|
||||
|
||||
// See git-rev-list(1) for description of the format string
|
||||
|
||||
var commitFilterArgs []string
|
||||
if deep {
|
||||
// Find all seal commits with their signing indicator
|
||||
cmd := m.cmdWithGPG(ctx, "git", "rev-list", `--pretty=format:%G?,%H`, "--no-commit-header", "--grep=Argocd-gpg-seal:", "--regexp-ignore-case", revisionSha)
|
||||
sealCommitsRawOut, err := m.runCmdOutput(cmd, runOpts{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
commitFilterArgs, err = m.getSealRevListFilter(revisionSha, sealCommitsRawOut)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
// List only the one revision - no seal commit search done
|
||||
commitFilterArgs = []string{revisionSha, "-1", "--"}
|
||||
}
|
||||
|
||||
// Find all commits until the criteria, including
|
||||
lsArgs := append([]string{"rev-list", `--pretty=format:%H,%G?,%GK,"%aD","%an <%ae>"`, "--no-commit-header"}, commitFilterArgs...)
|
||||
commitSignaturesRawOut, err := m.runCmdOutput(m.cmdWithGPG(ctx, "git", lsArgs...), runOpts{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return commitSignaturesRawOut, nil
|
||||
}
|
||||
|
||||
// getSealRevListFilter create arguments for `git rev-list` to search the history all the way until the seal commits found.
|
||||
func (m *nativeGitClient) getSealRevListFilter(revision string, sealCommitsRawOut string) ([]string, error) {
|
||||
// Keep only seal commits with a valid signature
|
||||
var sealCommits []string
|
||||
for line := range strings.SplitSeq(sealCommitsRawOut, "\n") {
|
||||
if strings.HasPrefix(line, "G,") {
|
||||
sealCommits = append(sealCommits, line[2:])
|
||||
}
|
||||
}
|
||||
sealCommitsLen := len(sealCommits)
|
||||
log.Debugf("Found %d seal commits for %s", sealCommitsLen, revision)
|
||||
|
||||
// No (correctly signed) seal commits found - verify all ancestry
|
||||
if sealCommitsLen == 0 {
|
||||
return []string{revision, "--"}, nil
|
||||
}
|
||||
|
||||
// Resolve, in case revision is not a commit number
|
||||
sha, err := m.CommitSHA()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sha == sealCommits[0] {
|
||||
// Currently on seal commit - verify just this one
|
||||
return []string{revision, "-1", "--"}, nil
|
||||
}
|
||||
|
||||
// Some seal commits in history - filter until those
|
||||
return append([]string{"--boundary", revision, "--not"}, sealCommits...), nil
|
||||
}
|
||||
|
||||
// IsAnnotatedTag returns true if the revision is an annotated tag existing in the repository, and false for everything else.
|
||||
func (m *nativeGitClient) IsAnnotatedTag(revision string) bool {
|
||||
cmd := exec.CommandContext(context.Background(), "git", "describe", "--exact-match", revision)
|
||||
cmd := exec.CommandContext(context.Background(), "git", "cat-file", "-t", revision)
|
||||
out, err := m.runCmdOutput(cmd, runOpts{SkipErrorLogging: true})
|
||||
if out != "" && err == nil {
|
||||
// a lightweight tag returns "commit" - makes sense in the git world
|
||||
if err == nil && out == "tag" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
@ -1247,11 +1567,11 @@ func (m *nativeGitClient) HasFileChanged(filePath string) (bool, error) {
|
|||
return false, fmt.Errorf("git diff failed: %w", err)
|
||||
}
|
||||
|
||||
// runWrapper runs a custom command with all the semantics of running the Git client
|
||||
func (m *nativeGitClient) runGnuPGWrapper(ctx context.Context, wrapper string, args ...string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, wrapper, args...)
|
||||
// cmdWithGPG creates git Cmd with a GPG-enabled environment
|
||||
func (m *nativeGitClient) cmdWithGPG(ctx context.Context, name string, args ...string) *exec.Cmd {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Env = append(cmd.Env, "GNUPGHOME="+common.GetGnuPGHomePath(), "LANG=C")
|
||||
return m.runCmdOutput(cmd, runOpts{})
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runCmd is a convenience function to run a command in a given directory and return its output
|
||||
|
|
|
|||
|
|
@ -121,25 +121,30 @@ func Test_IsAnnotatedTag(t *testing.T) {
|
|||
err = runCmd(ctx, client.Root(), "git", "commit", "-m", "Initial commit", "-a")
|
||||
require.NoError(t, err)
|
||||
|
||||
atag := client.IsAnnotatedTag("master")
|
||||
err = runCmd(ctx, client.Root(), "git", "tag", "annot-tag", "-a", "-m", "Create annotated tag")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = runCmd(ctx, client.Root(), "git", "tag", "light-tag")
|
||||
require.NoError(t, err)
|
||||
|
||||
atag := client.IsAnnotatedTag("HEAD")
|
||||
assert.False(t, atag)
|
||||
|
||||
err = runCmd(ctx, client.Root(), "git", "tag", "some-tag", "-a", "-m", "Create annotated tag")
|
||||
atag = client.IsAnnotatedTag("master")
|
||||
assert.False(t, atag)
|
||||
|
||||
atag = client.IsAnnotatedTag("blorp")
|
||||
assert.False(t, atag)
|
||||
|
||||
sha, err := client.CommitSHA()
|
||||
require.NoError(t, err)
|
||||
atag = client.IsAnnotatedTag("some-tag")
|
||||
atag = client.IsAnnotatedTag(sha)
|
||||
assert.False(t, atag)
|
||||
|
||||
atag = client.IsAnnotatedTag("annot-tag")
|
||||
assert.True(t, atag)
|
||||
|
||||
// Tag effectually points to HEAD, so it's considered the same
|
||||
atag = client.IsAnnotatedTag("HEAD")
|
||||
assert.True(t, atag)
|
||||
|
||||
err = runCmd(ctx, client.Root(), "git", "rm", "README")
|
||||
require.NoError(t, err)
|
||||
err = runCmd(ctx, client.Root(), "git", "commit", "-m", "remove README", "-a")
|
||||
require.NoError(t, err)
|
||||
|
||||
// We moved on, so tag doesn't point to HEAD anymore
|
||||
atag = client.IsAnnotatedTag("HEAD")
|
||||
atag = client.IsAnnotatedTag("light-tag")
|
||||
assert.False(t, atag)
|
||||
}
|
||||
|
||||
|
|
@ -1463,3 +1468,40 @@ func Test_nativeGitClient_HasFileChanged(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.True(t, changed, "expected modified file to be reported as changed")
|
||||
}
|
||||
|
||||
func Test_LsSignatures_Error(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
tempDir, err := _createEmptyGitRepo(ctx)
|
||||
require.NoError(t, err)
|
||||
client, err := NewClient("file://"+tempDir, NopCreds{}, true, false, "", "")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Init())
|
||||
out, err := client.SetAuthor("test", "test@example.com")
|
||||
require.NoError(t, err, "error output: %s", out)
|
||||
|
||||
err = runCmd(ctx, tempDir, "git", "log")
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
revision string
|
||||
deep bool
|
||||
expectedMsg string
|
||||
}{
|
||||
{
|
||||
revision: "5555.*",
|
||||
deep: false,
|
||||
expectedMsg: "version matching constraint not found",
|
||||
},
|
||||
{
|
||||
revision: "5555.*",
|
||||
deep: true,
|
||||
expectedMsg: "version matching constraint not found",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
signatures, err := client.LsSignatures(tt.revision, tt.deep)
|
||||
require.ErrorContains(t, err, tt.expectedMsg)
|
||||
assert.Nil(t, signatures)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -358,7 +358,7 @@ func TestLFSClient(t *testing.T) {
|
|||
func TestVerifyCommitSignature(t *testing.T) {
|
||||
p := t.TempDir()
|
||||
|
||||
client, err := NewClientExt("https://github.com/argoproj/argo-cd.git", p, NopCreds{}, false, false, "", "")
|
||||
client, err := NewClientExt("https://github.com/argoproj/argocd-example-apps.git", p, NopCreds{}, false, false, "", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Init()
|
||||
|
|
@ -375,8 +375,8 @@ func TestVerifyCommitSignature(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Fetch the specific commits needed for signature verification
|
||||
signedCommit := "28027897aad1262662096745f2ce2d4c74d02b7f"
|
||||
unsignedCommit := "85d660f0b967960becce3d49bd51c678ba2a5d24"
|
||||
signedCommit := "723b86e01bea11dcf72316cb172868fcbf05d69e"
|
||||
unsignedCommit := "1ccdee0a611224ccc6b9ff7919fe7002f905436e"
|
||||
err = client.Fetch(signedCommit, 1)
|
||||
require.NoError(t, err)
|
||||
err = client.Fetch(unsignedCommit, 1)
|
||||
|
|
|
|||
320
util/git/gpg_verification_test.go
Normal file
320
util/git/gpg_verification_test.go
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
)
|
||||
|
||||
type gpgReadyRepo struct {
|
||||
t *testing.T
|
||||
git Client
|
||||
gpgHome string
|
||||
}
|
||||
|
||||
func newGPGReadyRepo(t *testing.T) *gpgReadyRepo {
|
||||
t.Helper()
|
||||
repo := &gpgReadyRepo{t, nil, t.TempDir()}
|
||||
|
||||
t.Setenv(common.EnvGnuPGHome, repo.gpgHome)
|
||||
|
||||
err := os.Chmod(repo.gpgHome, 0o700)
|
||||
require.NoError(t, err)
|
||||
|
||||
repo.git, err = NewClient("https://fake.url/org/repo.git", NopCreds{}, true, false, "", "")
|
||||
require.NoError(t, err)
|
||||
_ = os.RemoveAll(repo.git.Root())
|
||||
t.Cleanup(func() {
|
||||
_ = os.RemoveAll(repo.git.Root())
|
||||
})
|
||||
err = repo.git.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, repo.cmd("checkout", "-b", "main"))
|
||||
repo.setUser("Test User", "test@example.com")
|
||||
|
||||
return repo
|
||||
}
|
||||
|
||||
func (g *gpgReadyRepo) setUser(name string, email string) {
|
||||
require.NoError(g.t, g.cmd("config", "--local", "user.name", name))
|
||||
require.NoError(g.t, g.cmd("config", "--local", "user.email", email))
|
||||
}
|
||||
|
||||
func (g *gpgReadyRepo) generateGPGKey(name string) (keyID string) {
|
||||
g.t.Helper()
|
||||
keyInput := fmt.Sprintf(`%%echo Generating test key
|
||||
Key-Type: RSA
|
||||
Key-Length: 2048
|
||||
Name-Real: %s User
|
||||
Name-Email: %s@example.com
|
||||
Expire-Date: 0
|
||||
%%no-protection
|
||||
%%commit
|
||||
%%echo Done`, name, name)
|
||||
|
||||
cmd := exec.CommandContext(g.t.Context(), "gpg", "--batch", "--generate-key", "--homedir", g.gpgHome)
|
||||
cmd.Stdin = strings.NewReader(keyInput)
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(g.t, err, "gpg key generation failed: %s", out)
|
||||
|
||||
cmd = exec.CommandContext(g.t.Context(), "gpg", "--list-keys", "--with-colons", "--homedir", g.gpgHome)
|
||||
out, err = cmd.Output()
|
||||
require.NoError(g.t, err)
|
||||
|
||||
// Parse output to get key ID
|
||||
lines := strings.SplitSeq(string(out), "\n")
|
||||
for line := range lines {
|
||||
if strings.HasPrefix(line, "pub:") {
|
||||
fields := strings.Split(line, ":")
|
||||
if len(fields) > 4 {
|
||||
keyID = fields[4]
|
||||
// Loop even after found intentionally, expected the newest key will be the last one
|
||||
}
|
||||
}
|
||||
}
|
||||
require.NotEmpty(g.t, keyID, "failed to get GPG key ID")
|
||||
|
||||
return keyID
|
||||
}
|
||||
|
||||
func (g *gpgReadyRepo) revokeGPGKey(keyID string) {
|
||||
cmd := exec.CommandContext(
|
||||
g.t.Context(),
|
||||
"gpg", "--batch",
|
||||
"--command-fd=0", "--status-fd=1",
|
||||
"--homedir", g.gpgHome,
|
||||
"--edit-key", keyID,
|
||||
)
|
||||
// gpg is so not meant to be used from automation. This is why `--command-fd=0 --status-fd=1` is needed
|
||||
cmd.Stdin = strings.NewReader(`revkey
|
||||
y
|
||||
2
|
||||
Automated revocation
|
||||
|
||||
y
|
||||
save
|
||||
`)
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(g.t, err, "gpg key revocation generation failed: %s", out)
|
||||
}
|
||||
|
||||
func (g *gpgReadyRepo) cmd(args ...string) error {
|
||||
cmd := exec.CommandContext(g.t.Context(), "git", args...)
|
||||
cmd.Dir = g.git.Root()
|
||||
cmd.Env = append(cmd.Env, "GNUPGHOME="+g.gpgHome)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (g *gpgReadyRepo) commitSHA() string {
|
||||
sha, err := g.git.CommitSHA()
|
||||
require.NoError(g.t, err)
|
||||
return sha
|
||||
}
|
||||
|
||||
func (g *gpgReadyRepo) assertSignedAs(revision string, expectedSign ...string) {
|
||||
info, err := g.git.LsSignatures(revision, true)
|
||||
require.NoError(g.t, err)
|
||||
|
||||
var actualSign []string
|
||||
for _, record := range info {
|
||||
actualSign = append(actualSign, string(record.VerificationResult)+"="+record.SignatureKeyID)
|
||||
}
|
||||
assert.Equal(g.t, expectedSign, actualSign)
|
||||
}
|
||||
|
||||
func Test_LsSignatures_SignedAndMerged(t *testing.T) {
|
||||
repo := newGPGReadyRepo(t)
|
||||
mainKeyID := repo.generateGPGKey("main")
|
||||
otherKeyID := repo.generateGPGKey("other")
|
||||
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=root", "--gpg-sign="+mainKeyID))
|
||||
|
||||
require.NoError(t, repo.cmd("checkout", "-b", "left", "main"))
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=right", "--gpg-sign="+otherKeyID))
|
||||
|
||||
require.NoError(t, repo.cmd("checkout", "main"))
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=left", "--gpg-sign="+mainKeyID))
|
||||
|
||||
require.NoError(t, repo.cmd("merge", "left", "--no-edit", "--message=merge", "--gpg-sign="+mainKeyID))
|
||||
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=main", "--gpg-sign="+mainKeyID))
|
||||
|
||||
tip := repo.commitSHA()
|
||||
|
||||
repo.assertSignedAs(
|
||||
tip,
|
||||
"signed="+mainKeyID, // main
|
||||
"signed="+mainKeyID, // merge
|
||||
"signed="+mainKeyID, "signed="+otherKeyID, // left + right
|
||||
"signed="+mainKeyID, // root
|
||||
)
|
||||
|
||||
repo.revokeGPGKey(mainKeyID)
|
||||
repo.assertSignedAs(
|
||||
tip,
|
||||
"signed with revoked key="+mainKeyID, // main
|
||||
"signed with revoked key="+mainKeyID, // merge
|
||||
"signed with revoked key="+mainKeyID, "signed="+otherKeyID, // left + right
|
||||
"signed with revoked key="+mainKeyID, // root
|
||||
)
|
||||
}
|
||||
|
||||
func Test_LsSignatures_Sealed_linear(t *testing.T) {
|
||||
repo := newGPGReadyRepo(t)
|
||||
trustedKeyID := repo.generateGPGKey("trusted")
|
||||
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=signed", "--gpg-sign="+trustedKeyID))
|
||||
repo.assertSignedAs(repo.commitSHA(), "signed="+trustedKeyID)
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=unsigned"))
|
||||
repo.assertSignedAs(repo.commitSHA(), "unsigned=", "signed="+trustedKeyID)
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=signed", "--gpg-sign="+trustedKeyID))
|
||||
repo.assertSignedAs(repo.commitSHA(), "signed="+trustedKeyID, "unsigned=", "signed="+trustedKeyID)
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=seal", "--gpg-sign="+trustedKeyID, "--trailer=ArgoCD-gpg-seal: XXX"))
|
||||
repo.assertSignedAs(repo.commitSHA(), "signed="+trustedKeyID)
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=signed", "--gpg-sign="+trustedKeyID))
|
||||
repo.assertSignedAs(repo.commitSHA(), "signed="+trustedKeyID, "signed="+trustedKeyID)
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=unsigned"))
|
||||
repo.assertSignedAs(repo.commitSHA(), "unsigned=", "signed="+trustedKeyID, "signed="+trustedKeyID)
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=seal", "--gpg-sign="+trustedKeyID, "--trailer=ArgoCD-gpg-seal: XXX"))
|
||||
repo.assertSignedAs(repo.commitSHA(), "signed="+trustedKeyID)
|
||||
}
|
||||
|
||||
func Test_LsSignatures_UnsignedSealedCommitDoesNotStopHistorySearch(t *testing.T) {
|
||||
// The seal commit must be signed and trusted. When it is not, it is not considered a seal commit and the history is searched further.
|
||||
repo := newGPGReadyRepo(t)
|
||||
trustedKeyID := repo.generateGPGKey("trusted")
|
||||
|
||||
// Will not be listed
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=unsigned init"))
|
||||
// The seal commit we stop on
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=signed seal", "--trailer=ArgoCD-gpg-seal: XXX", "--gpg-sign="+trustedKeyID))
|
||||
signedSealSha := repo.commitSHA()
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=unsigned past"))
|
||||
unsignedPastSha := repo.commitSHA()
|
||||
// The wannabe seal commit we ignore - unsigned
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=unsigned seal", "--trailer=ArgoCD-gpg-seal: XXX"))
|
||||
unsignedSealSha := repo.commitSHA()
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=signed", "--gpg-sign="+trustedKeyID))
|
||||
signedSha := repo.commitSHA()
|
||||
|
||||
info, err := repo.git.LsSignatures(signedSha, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, info, 4)
|
||||
assert.Equal(t, GPGVerificationResultGood, info[0].VerificationResult)
|
||||
assert.Equal(t, signedSha, info[0].Revision)
|
||||
|
||||
assert.Equal(t, GPGVerificationResultUnsigned, info[1].VerificationResult)
|
||||
assert.Equal(t, unsignedSealSha, info[1].Revision)
|
||||
|
||||
assert.Equal(t, GPGVerificationResultUnsigned, info[2].VerificationResult)
|
||||
assert.Equal(t, unsignedPastSha, info[2].Revision)
|
||||
|
||||
assert.Equal(t, GPGVerificationResultGood, info[3].VerificationResult)
|
||||
assert.Equal(t, signedSealSha, info[3].Revision)
|
||||
}
|
||||
|
||||
func Test_SignedTag(t *testing.T) {
|
||||
repo := newGPGReadyRepo(t)
|
||||
commitKeyId := repo.generateGPGKey("commit gpg")
|
||||
tagKeyId := repo.generateGPGKey("tag gpg")
|
||||
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--message=unsigned"))
|
||||
require.NoError(t, repo.cmd("commit", "--allow-empty", "--message=signed", "--gpg-sign="+commitKeyId))
|
||||
|
||||
// Tags are made by different user and key
|
||||
repo.setUser("Tagging user", "tagger@argo.io")
|
||||
require.NoError(t, repo.cmd("tag", "--message=signed tag", "--local-user="+tagKeyId, "1.0", "HEAD~1"))
|
||||
require.NoError(t, repo.cmd("tag", "--message=signed tag", "--local-user="+tagKeyId, "2.0", "HEAD"))
|
||||
require.NoError(t, repo.cmd("tag", "--message=unsigned tag", "dev", "HEAD"))
|
||||
|
||||
info, err := repo.git.LsSignatures("1.0", false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, info, 1)
|
||||
assert.Equal(t, "1.0", info[0].Revision)
|
||||
assert.Equal(t, GPGVerificationResultGood, info[0].VerificationResult)
|
||||
assert.Equal(t, tagKeyId, info[0].SignatureKeyID)
|
||||
assert.Equal(t, `Tagging user "<tagger@argo.io>"`, info[0].AuthorIdentity)
|
||||
|
||||
info, err = repo.git.LsSignatures("2.0", false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, info, 1)
|
||||
assert.Equal(t, "2.0", info[0].Revision)
|
||||
assert.Equal(t, GPGVerificationResultGood, info[0].VerificationResult)
|
||||
assert.Equal(t, tagKeyId, info[0].SignatureKeyID)
|
||||
assert.Equal(t, `Tagging user "<tagger@argo.io>"`, info[0].AuthorIdentity)
|
||||
|
||||
info, err = repo.git.LsSignatures("dev", false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, info, 1)
|
||||
assert.Equal(t, "dev", info[0].Revision)
|
||||
assert.Equal(t, GPGVerificationResultUnsigned, info[0].VerificationResult)
|
||||
assert.Empty(t, info[0].SignatureKeyID)
|
||||
assert.Equal(t, `Tagging user "<tagger@argo.io>"`, info[0].AuthorIdentity)
|
||||
}
|
||||
|
||||
func Test_parseGpgSignStatus(t *testing.T) {
|
||||
testCases := []struct {
|
||||
cmdErr error
|
||||
tagGpgOut string
|
||||
expError string
|
||||
expResult GPGVerificationResult
|
||||
expKeyID string
|
||||
}{
|
||||
{
|
||||
errors.New("fake"),
|
||||
"error: no signature found",
|
||||
"", GPGVerificationResultUnsigned, "",
|
||||
},
|
||||
{
|
||||
errors.New("fake"),
|
||||
"the unexpected have happened",
|
||||
"fake", "", "",
|
||||
},
|
||||
{
|
||||
nil,
|
||||
"Buahahaha!",
|
||||
"unexpected `git verify-tag --raw` output: \"Buahahaha!\"", "", "",
|
||||
},
|
||||
{
|
||||
nil,
|
||||
`[GNUPG:] NEWSIG
|
||||
[GNUPG:] ERRSIG D56C4FCA57A46444 1 10 00 1763632400 9 EA459B49595CBE3FD1FBA303D56C4FCA57A46444
|
||||
[GNUPG:] NO_PUBKEY D56C4FCA57A46444
|
||||
[GNUPG:] FAILURE gpg-exit 33554433`,
|
||||
"", GPGVerificationResultMissingKey, "D56C4FCA57A46444",
|
||||
},
|
||||
{
|
||||
nil,
|
||||
`[GNUPG:] NEWSIG user17@argo.io
|
||||
[GNUPG:] KEY_CONSIDERED D7E87AF6B99E64079FFECC029515ACB41E14E7F9 0
|
||||
[GNUPG:] SIG_ID ES7wSYaAnVXVsRjW15LzE4TMp+U 2025-11-19 3671527729
|
||||
[GNUPG:] GOODSIG 9515ACB41E14E7F9 User N17 <user17@argo.io>
|
||||
[GNUPG:] VALIDSIG D7E87AF6B99E64079FFECC029515ACB41E14E7F9 2025-11-19 3671527729 0 4 0 1 10 00 D7E87AF6B99E64079FFECC029515ACB41E14E7F9
|
||||
[GNUPG:] TRUST_ULTIMATE 0 pgp user17@argo.io`,
|
||||
"", GPGVerificationResultGood, "9515ACB41E14E7F9",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
result, keyId, err := evaluateGpgSignStatus(tt.cmdErr, tt.tagGpgOut)
|
||||
|
||||
if tt.expError != "" {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tt.expError, err.Error())
|
||||
}
|
||||
assert.Equal(t, tt.expResult, result)
|
||||
assert.Equal(t, tt.expKeyID, keyId)
|
||||
}
|
||||
}
|
||||
128
util/git/mocks/Client.go
generated
128
util/git/mocks/Client.go
generated
|
|
@ -724,8 +724,8 @@ func (_c *Client_Init_Call) RunAndReturn(run func() error) *Client_Init_Call {
|
|||
}
|
||||
|
||||
// IsAnnotatedTag provides a mock function for the type Client
|
||||
func (_mock *Client) IsAnnotatedTag(s string) bool {
|
||||
ret := _mock.Called(s)
|
||||
func (_mock *Client) IsAnnotatedTag(revision string) bool {
|
||||
ret := _mock.Called(revision)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for IsAnnotatedTag")
|
||||
|
|
@ -733,7 +733,7 @@ func (_mock *Client) IsAnnotatedTag(s string) bool {
|
|||
|
||||
var r0 bool
|
||||
if returnFunc, ok := ret.Get(0).(func(string) bool); ok {
|
||||
r0 = returnFunc(s)
|
||||
r0 = returnFunc(revision)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
|
@ -746,12 +746,12 @@ type Client_IsAnnotatedTag_Call struct {
|
|||
}
|
||||
|
||||
// IsAnnotatedTag is a helper method to define mock.On call
|
||||
// - s string
|
||||
func (_e *Client_Expecter) IsAnnotatedTag(s interface{}) *Client_IsAnnotatedTag_Call {
|
||||
return &Client_IsAnnotatedTag_Call{Call: _e.mock.On("IsAnnotatedTag", s)}
|
||||
// - revision string
|
||||
func (_e *Client_Expecter) IsAnnotatedTag(revision interface{}) *Client_IsAnnotatedTag_Call {
|
||||
return &Client_IsAnnotatedTag_Call{Call: _e.mock.On("IsAnnotatedTag", revision)}
|
||||
}
|
||||
|
||||
func (_c *Client_IsAnnotatedTag_Call) Run(run func(s string)) *Client_IsAnnotatedTag_Call {
|
||||
func (_c *Client_IsAnnotatedTag_Call) Run(run func(revision string)) *Client_IsAnnotatedTag_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 string
|
||||
if args[0] != nil {
|
||||
|
|
@ -769,7 +769,7 @@ func (_c *Client_IsAnnotatedTag_Call) Return(b bool) *Client_IsAnnotatedTag_Call
|
|||
return _c
|
||||
}
|
||||
|
||||
func (_c *Client_IsAnnotatedTag_Call) RunAndReturn(run func(s string) bool) *Client_IsAnnotatedTag_Call {
|
||||
func (_c *Client_IsAnnotatedTag_Call) RunAndReturn(run func(revision string) bool) *Client_IsAnnotatedTag_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
|
@ -1063,6 +1063,74 @@ func (_c *Client_LsRemote_Call) RunAndReturn(run func(revision string) (string,
|
|||
return _c
|
||||
}
|
||||
|
||||
// LsSignatures provides a mock function for the type Client
|
||||
func (_mock *Client) LsSignatures(revision string, deep bool) ([]git.RevisionSignatureInfo, error) {
|
||||
ret := _mock.Called(revision, deep)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for LsSignatures")
|
||||
}
|
||||
|
||||
var r0 []git.RevisionSignatureInfo
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(string, bool) ([]git.RevisionSignatureInfo, error)); ok {
|
||||
return returnFunc(revision, deep)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(string, bool) []git.RevisionSignatureInfo); ok {
|
||||
r0 = returnFunc(revision, deep)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]git.RevisionSignatureInfo)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(string, bool) error); ok {
|
||||
r1 = returnFunc(revision, deep)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Client_LsSignatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LsSignatures'
|
||||
type Client_LsSignatures_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// LsSignatures is a helper method to define mock.On call
|
||||
// - revision string
|
||||
// - deep bool
|
||||
func (_e *Client_Expecter) LsSignatures(revision interface{}, deep interface{}) *Client_LsSignatures_Call {
|
||||
return &Client_LsSignatures_Call{Call: _e.mock.On("LsSignatures", revision, deep)}
|
||||
}
|
||||
|
||||
func (_c *Client_LsSignatures_Call) Run(run func(revision string, deep bool)) *Client_LsSignatures_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 string
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(string)
|
||||
}
|
||||
var arg1 bool
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(bool)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Client_LsSignatures_Call) Return(revisionSignatureInfos []git.RevisionSignatureInfo, err error) *Client_LsSignatures_Call {
|
||||
_c.Call.Return(revisionSignatureInfos, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Client_LsSignatures_Call) RunAndReturn(run func(revision string, deep bool) ([]git.RevisionSignatureInfo, error)) *Client_LsSignatures_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoveContents provides a mock function for the type Client
|
||||
func (_mock *Client) RemoveContents(paths []string) (string, error) {
|
||||
ret := _mock.Called(paths)
|
||||
|
|
@ -1123,6 +1191,50 @@ func (_c *Client_RemoveContents_Call) RunAndReturn(run func(paths []string) (str
|
|||
return _c
|
||||
}
|
||||
|
||||
// RepoURL provides a mock function for the type Client
|
||||
func (_mock *Client) RepoURL() string {
|
||||
ret := _mock.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RepoURL")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if returnFunc, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = returnFunc()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Client_RepoURL_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RepoURL'
|
||||
type Client_RepoURL_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RepoURL is a helper method to define mock.On call
|
||||
func (_e *Client_Expecter) RepoURL() *Client_RepoURL_Call {
|
||||
return &Client_RepoURL_Call{Call: _e.mock.On("RepoURL")}
|
||||
}
|
||||
|
||||
func (_c *Client_RepoURL_Call) Run(run func()) *Client_RepoURL_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Client_RepoURL_Call) Return(s string) *Client_RepoURL_Call {
|
||||
_c.Call.Return(s)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Client_RepoURL_Call) RunAndReturn(run func() string) *Client_RepoURL_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RevisionMetadata provides a mock function for the type Client
|
||||
func (_mock *Client) RevisionMetadata(revision string) (*git.RevisionMetadata, error) {
|
||||
ret := _mock.Called(revision)
|
||||
|
|
|
|||
3
util/gpg/testdata/bad_signature_bad.txt
vendored
3
util/gpg/testdata/bad_signature_bad.txt
vendored
|
|
@ -1,3 +0,0 @@
|
|||
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
|
||||
gpg: using RSA key 4AEE18F83AFDEB23
|
||||
gpg: BAD signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]
|
||||
3
util/gpg/testdata/bad_signature_badkeyid.txt
vendored
3
util/gpg/testdata/bad_signature_badkeyid.txt
vendored
|
|
@ -1,3 +0,0 @@
|
|||
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
|
||||
gpg: using RSA key 5F4AEE18F83AFDEB23
|
||||
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
gpg: Signature was made Wed Feb 26 23:22:34 2020 CET
|
||||
gpg: using RSA key 4AEE18F83AFDEB23
|
||||
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
|
||||
gpg: using RSA key noreply@github.com
|
||||
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
|
||||
gpg: using RSA key 4AEE18F83AFDEB23
|
||||
gpg: Good signature from "GitHub (web-flow commit signing)" <noreply@github.com>" [ultimate]
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
gpg: CRC error; AF65FD - 3ABB26
|
||||
gpg: [don't know]: invalid packet (ctb=78)
|
||||
gpg: no signature found
|
||||
gpg: the signature could not be verified.
|
||||
Please remember that the signature file (.sig or .asc)
|
||||
should be the first file given on the command line.
|
||||
3
util/gpg/testdata/bad_signature_nodata.txt
vendored
3
util/gpg/testdata/bad_signature_nodata.txt
vendored
|
|
@ -1,3 +0,0 @@
|
|||
Lorem ipsum
|
||||
Lorem ipsum
|
||||
Lorem ipsum
|
||||
2
util/gpg/testdata/bad_signature_preeof1.txt
vendored
2
util/gpg/testdata/bad_signature_preeof1.txt
vendored
|
|
@ -1,2 +0,0 @@
|
|||
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
|
||||
gpg: using RSA key 4AEE18F83AFDEB23
|
||||
1
util/gpg/testdata/bad_signature_preeof2.txt
vendored
1
util/gpg/testdata/bad_signature_preeof2.txt
vendored
|
|
@ -1 +0,0 @@
|
|||
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
|
||||
59
util/gpg/testdata/data.go
vendored
59
util/gpg/testdata/data.go
vendored
|
|
@ -1,59 +0,0 @@
|
|||
package testdata
|
||||
|
||||
import _ "embed"
|
||||
|
||||
var (
|
||||
//go:embed bad_signature_bad.txt
|
||||
Bad_signature_bad_txt string
|
||||
|
||||
//go:embed bad_signature_badkeyid.txt
|
||||
Bad_signature_badkeyid_txt string
|
||||
|
||||
//go:embed bad_signature_malformed1.txt
|
||||
Bad_signature_malformed1_txt string
|
||||
|
||||
//go:embed bad_signature_malformed2.txt
|
||||
Bad_signature_malformed2_txt string
|
||||
|
||||
//go:embed bad_signature_malformed3.txt
|
||||
Bad_signature_malformed3_txt string
|
||||
|
||||
//go:embed bad_signature_manipulated.txt
|
||||
Bad_signature_manipulated_txt string
|
||||
|
||||
//go:embed bad_signature_nodata.txt
|
||||
Bad_signature_nodata_txt string
|
||||
|
||||
//go:embed bad_signature_preeof1.txt
|
||||
Bad_signature_preeof1_txt string
|
||||
|
||||
//go:embed bad_signature_preeof2.txt
|
||||
Bad_signature_preeof2_txt string
|
||||
|
||||
//go:embed garbage.asc
|
||||
Garbage_asc string
|
||||
|
||||
//go:embed github.asc
|
||||
Github_asc string
|
||||
|
||||
//go:embed good_signature.txt
|
||||
Good_signature_txt string
|
||||
|
||||
//go:embed janedoe.asc
|
||||
Janedoe_asc string
|
||||
|
||||
//go:embed johndoe.asc
|
||||
Johndoe_asc string
|
||||
|
||||
//go:embed multi.asc
|
||||
Multi_asc string
|
||||
|
||||
//go:embed multi2.asc
|
||||
Multi2_asc string
|
||||
|
||||
//go:embed unknown_signature1.txt
|
||||
Unknown_signature1_txt string
|
||||
|
||||
//go:embed unknown_signature2.txt
|
||||
Unknown_signature2_txt string
|
||||
)
|
||||
3
util/gpg/testdata/good_signature.txt
vendored
3
util/gpg/testdata/good_signature.txt
vendored
|
|
@ -1,3 +0,0 @@
|
|||
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
|
||||
gpg: using RSA key 4AEE18F83AFDEB23
|
||||
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]
|
||||
3
util/gpg/testdata/unknown_signature1.txt
vendored
3
util/gpg/testdata/unknown_signature1.txt
vendored
|
|
@ -1,3 +0,0 @@
|
|||
gpg: Signature made Mon Aug 26 20:59:48 2019 CEST
|
||||
gpg: using RSA key 4AEE18F83AFDEB23
|
||||
gpg: Can't check signature: No public key
|
||||
4
util/gpg/testdata/unknown_signature2.txt
vendored
4
util/gpg/testdata/unknown_signature2.txt
vendored
|
|
@ -1,4 +0,0 @@
|
|||
gpg: Signature made Mon Aug 26 20:59:48 2019 CEST
|
||||
gpg: using RSA key 4AEE18F83AFDEB23
|
||||
gpg: issuer "j.doe@example.com"
|
||||
gpg: Can't check signature: No public key
|
||||
|
|
@ -285,9 +285,8 @@ g, depB, role:depB
|
|||
`
|
||||
hook := test.LogHook{}
|
||||
log.AddHook(&hook)
|
||||
t.Cleanup(func() {
|
||||
log.StandardLogger().ReplaceHooks(log.LevelHooks{})
|
||||
})
|
||||
t.Cleanup(hook.CleanupHook)
|
||||
|
||||
require.NoError(t, ValidatePolicy(policy))
|
||||
assert.Empty(t, hook.GetRegexMatchesInEntries("user defined roles not found in policies"))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package gpg
|
||||
package sourceintegrity
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
|
@ -32,18 +32,6 @@ var uidMatch = regexp.MustCompile(`^uid\s*\[\s*([a-z]+)\s*\]\s+(.*)$`)
|
|||
// Regular expression to match import status
|
||||
var importMatch = regexp.MustCompile(`^gpg: key ([A-Z0-9]+): public key "([^"]+)" imported$`)
|
||||
|
||||
// Regular expression to match the start of a commit signature verification
|
||||
var verificationStartMatch = regexp.MustCompile(`^gpg: Signature made ([a-zA-Z0-9\ :]+)$`)
|
||||
|
||||
// Regular expression to match the key ID of a commit signature verification
|
||||
var verificationKeyIDMatch = regexp.MustCompile(`^gpg:\s+using\s([A-Za-z]+)\skey\s([a-zA-Z0-9]+)$`)
|
||||
|
||||
// Regular expression to match possible additional fields of a commit signature verification
|
||||
var verificationAdditionalFields = regexp.MustCompile(`^gpg:\s+issuer\s.+$`)
|
||||
|
||||
// Regular expression to match the signature status of a commit signature verification
|
||||
var verificationStatusMatch = regexp.MustCompile(`^gpg: ([a-zA-Z]+) signature from "([^"]+)" \[([a-zA-Z]+)\]$`)
|
||||
|
||||
// This is the recipe for automatic key generation, passed to gpg --batch --gen-key
|
||||
// for initializing our keyring with a trustdb. A new private key will be generated each
|
||||
// time argocd-server starts, so it's transient and is not used for anything except for
|
||||
|
|
@ -70,15 +58,15 @@ func isHexString(s string) bool {
|
|||
return err == nil
|
||||
}
|
||||
|
||||
// KeyID get the actual correct (short) key ID from either a fingerprint or the key ID. Returns the empty string if k seems not to be a PGP key ID.
|
||||
func KeyID(k string) string {
|
||||
// KeyID get the actual correct (short) key ID from either a fingerprint or the key ID. Errors if it is not a valid GnuPG key ID.
|
||||
func KeyID(k string) (string, error) {
|
||||
if IsLongKeyID(k) {
|
||||
return k[24:]
|
||||
return k[24:], nil
|
||||
} else if IsShortKeyID(k) {
|
||||
return k
|
||||
return k, nil
|
||||
}
|
||||
// Invalid key
|
||||
return ""
|
||||
return "", fmt.Errorf("'%s' is not a valid GnuPG key ID", k)
|
||||
}
|
||||
|
||||
// IsLongKeyID returns true if the string represents a long key ID (aka fingerprint)
|
||||
|
|
@ -97,32 +85,6 @@ func IsShortKeyID(k string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Result of a git commit verification
|
||||
type PGPVerifyResult struct {
|
||||
// Date the signature was made
|
||||
Date string
|
||||
// KeyID the signature was made with
|
||||
KeyID string
|
||||
// Identity
|
||||
Identity string
|
||||
// Trust level of the key
|
||||
Trust string
|
||||
// Cipher of the key the signature was made with
|
||||
Cipher string
|
||||
// Result of verification - "unknown", "good" or "bad"
|
||||
Result string
|
||||
// Additional informational message
|
||||
Message string
|
||||
}
|
||||
|
||||
// Signature verification results
|
||||
const (
|
||||
VerifyResultGood = "Good"
|
||||
VerifyResultBad = "Bad"
|
||||
VerifyResultInvalid = "Invalid"
|
||||
VerifyResultUnknown = "Unknown"
|
||||
)
|
||||
|
||||
// Key trust values
|
||||
const (
|
||||
TrustUnknown = "unknown"
|
||||
|
|
@ -141,9 +103,6 @@ var pgpTrustLevels = map[string]int{
|
|||
TrustUltimate: 6,
|
||||
}
|
||||
|
||||
// Maximum number of lines to parse for a gpg verify-commit output
|
||||
const MaxVerificationLinesToParse = 40
|
||||
|
||||
// Helper function to append GNUPGHOME for a command execution environment
|
||||
func getGPGEnviron() []string {
|
||||
return append(os.Environ(), "GNUPGHOME="+common.GetGnuPGHomePath(), "LANG=C")
|
||||
|
|
@ -205,14 +164,6 @@ func removeKeyRing(path string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// IsGPGEnabled returns true if GPG feature is enabled
|
||||
func IsGPGEnabled() bool {
|
||||
if en := os.Getenv("ARGOCD_GPG_ENABLED"); strings.EqualFold(en, "false") || strings.EqualFold(en, "no") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// InitializeGnuPG will initialize a GnuPG working directory and also create a
|
||||
// transient private key so that the trust DB will work correctly.
|
||||
func InitializeGnuPG() error {
|
||||
|
|
@ -481,7 +432,7 @@ func IsSecretKey(keyID string) (bool, error) {
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// GetInstalledPGPKeys() runs gpg to retrieve public keys from our keyring. If kids is non-empty, limit result to those key IDs
|
||||
// GetInstalledPGPKeys runs gpg to retrieve public keys from our keyring. If kids is non-empty, limit result to those key IDs
|
||||
func GetInstalledPGPKeys(kids []string) ([]*appsv1.GnuPGPublicKey, error) {
|
||||
keys := make([]*appsv1.GnuPGPublicKey, 0)
|
||||
ctx := context.Background()
|
||||
|
|
@ -580,109 +531,6 @@ func GetInstalledPGPKeys(kids []string) ([]*appsv1.GnuPGPublicKey, error) {
|
|||
return keys, nil
|
||||
}
|
||||
|
||||
// ParseGitCommitVerification parses the output of "git verify-commit" and returns the result
|
||||
func ParseGitCommitVerification(signature string) PGPVerifyResult {
|
||||
result := PGPVerifyResult{Result: VerifyResultUnknown}
|
||||
parseOk := false
|
||||
linesParsed := 0
|
||||
|
||||
// Shortcut for returning an unknown verification result with a reason
|
||||
unknownResult := func(reason string) PGPVerifyResult {
|
||||
return PGPVerifyResult{
|
||||
Result: VerifyResultUnknown,
|
||||
Message: reason,
|
||||
}
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(signature))
|
||||
for scanner.Scan() && linesParsed < MaxVerificationLinesToParse {
|
||||
linesParsed++
|
||||
|
||||
// Indicating the beginning of a signature
|
||||
start := verificationStartMatch.FindStringSubmatch(scanner.Text())
|
||||
if len(start) == 2 {
|
||||
result.Date = start[1]
|
||||
if !scanner.Scan() {
|
||||
return unknownResult("Unexpected end-of-file while parsing commit verification output.")
|
||||
}
|
||||
|
||||
linesParsed++
|
||||
|
||||
// What key has made the signature?
|
||||
keyID := verificationKeyIDMatch.FindStringSubmatch(scanner.Text())
|
||||
if len(keyID) != 3 {
|
||||
return unknownResult("Could not parse key ID of commit verification output.")
|
||||
}
|
||||
|
||||
result.Cipher = keyID[1]
|
||||
result.KeyID = KeyID(keyID[2])
|
||||
if result.KeyID == "" {
|
||||
return unknownResult("Invalid PGP key ID found in verification result: " + result.KeyID)
|
||||
}
|
||||
|
||||
// What was the result of signature verification?
|
||||
if !scanner.Scan() {
|
||||
return unknownResult("Unexpected end-of-file while parsing commit verification output.")
|
||||
}
|
||||
|
||||
linesParsed++
|
||||
|
||||
// Skip additional fields
|
||||
for verificationAdditionalFields.MatchString(scanner.Text()) {
|
||||
if !scanner.Scan() {
|
||||
return unknownResult("Unexpected end-of-file while parsing commit verification output.")
|
||||
}
|
||||
|
||||
linesParsed++
|
||||
}
|
||||
|
||||
if strings.HasPrefix(scanner.Text(), "gpg: Can't check signature: ") {
|
||||
result.Result = VerifyResultInvalid
|
||||
result.Identity = "unknown"
|
||||
result.Trust = TrustUnknown
|
||||
result.Message = scanner.Text()
|
||||
} else {
|
||||
sigState := verificationStatusMatch.FindStringSubmatch(scanner.Text())
|
||||
if len(sigState) != 4 {
|
||||
return unknownResult("Could not parse result of verify operation, check logs for more information.")
|
||||
}
|
||||
|
||||
switch strings.ToLower(sigState[1]) {
|
||||
case "good":
|
||||
result.Result = VerifyResultGood
|
||||
case "bad":
|
||||
result.Result = VerifyResultBad
|
||||
default:
|
||||
result.Result = VerifyResultInvalid
|
||||
}
|
||||
result.Identity = sigState[2]
|
||||
|
||||
// Did we catch a valid trust?
|
||||
if _, ok := pgpTrustLevels[sigState[3]]; ok {
|
||||
result.Trust = sigState[3]
|
||||
} else {
|
||||
result.Trust = TrustUnknown
|
||||
}
|
||||
result.Message = "Success verifying the commit signature."
|
||||
}
|
||||
|
||||
// No more data to parse here
|
||||
parseOk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if parseOk && linesParsed < MaxVerificationLinesToParse {
|
||||
// Operation successful - return result
|
||||
return result
|
||||
} else if linesParsed >= MaxVerificationLinesToParse {
|
||||
// Too many output lines, return error
|
||||
return unknownResult("Too many lines of gpg verify-commit output, abort.")
|
||||
}
|
||||
// No data found, return error
|
||||
return unknownResult("Could not parse output of verify-commit, no verification data found.")
|
||||
}
|
||||
|
||||
// SyncKeyRingFromDirectory will sync the GPG keyring with files in a directory. This is a one-way sync,
|
||||
// with the configuration being the leading information.
|
||||
// Files must have a file name matching their Key ID. Keys that are found in the directory but are not
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package gpg
|
||||
package sourceintegrity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -46,23 +46,6 @@ func initTempDir(t *testing.T) string {
|
|||
return p
|
||||
}
|
||||
|
||||
func Test_IsGPGEnabled(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
assert.True(t, IsGPGEnabled())
|
||||
})
|
||||
|
||||
t.Run("false", func(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "false")
|
||||
assert.False(t, IsGPGEnabled())
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "")
|
||||
assert.True(t, IsGPGEnabled())
|
||||
})
|
||||
}
|
||||
|
||||
func Test_GPG_InitializeGnuPG(t *testing.T) {
|
||||
p := initTempDir(t)
|
||||
|
||||
|
|
@ -296,161 +279,6 @@ func Test_ValidateGPGKeys(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_GPG_ParseGitCommitVerification(t *testing.T) {
|
||||
initTempDir(t)
|
||||
|
||||
err := InitializeGnuPG()
|
||||
require.NoError(t, err)
|
||||
|
||||
keys, err := ImportPGPKeys("testdata/github.asc")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
|
||||
// Good case
|
||||
{
|
||||
c, err := os.ReadFile("testdata/good_signature.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
|
||||
assert.Equal(t, "RSA", res.Cipher)
|
||||
assert.Equal(t, "ultimate", res.Trust)
|
||||
assert.Equal(t, "Wed Feb 26 23:22:34 2020 CET", res.Date)
|
||||
assert.Equal(t, VerifyResultGood, res.Result)
|
||||
}
|
||||
|
||||
// Signature with unknown key - considered invalid
|
||||
{
|
||||
c, err := os.ReadFile("testdata/unknown_signature1.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
|
||||
assert.Equal(t, "RSA", res.Cipher)
|
||||
assert.Equal(t, TrustUnknown, res.Trust)
|
||||
assert.Equal(t, "Mon Aug 26 20:59:48 2019 CEST", res.Date)
|
||||
assert.Equal(t, VerifyResultInvalid, res.Result)
|
||||
}
|
||||
|
||||
// Signature with unknown key and additional fields - considered invalid
|
||||
{
|
||||
c, err := os.ReadFile("testdata/unknown_signature2.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
|
||||
assert.Equal(t, "RSA", res.Cipher)
|
||||
assert.Equal(t, TrustUnknown, res.Trust)
|
||||
assert.Equal(t, "Mon Aug 26 20:59:48 2019 CEST", res.Date)
|
||||
assert.Equal(t, VerifyResultInvalid, res.Result)
|
||||
}
|
||||
|
||||
// Bad signature with known key
|
||||
{
|
||||
c, err := os.ReadFile("testdata/bad_signature_bad.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
|
||||
assert.Equal(t, "RSA", res.Cipher)
|
||||
assert.Equal(t, "ultimate", res.Trust)
|
||||
assert.Equal(t, "Wed Feb 26 23:22:34 2020 CET", res.Date)
|
||||
assert.Equal(t, VerifyResultBad, res.Result)
|
||||
}
|
||||
|
||||
// Bad case: Manipulated/invalid clear text signature
|
||||
{
|
||||
c, err := os.ReadFile("testdata/bad_signature_manipulated.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, VerifyResultUnknown, res.Result)
|
||||
assert.Contains(t, res.Message, "Could not parse output")
|
||||
}
|
||||
|
||||
// Bad case: Incomplete signature data #1
|
||||
{
|
||||
c, err := os.ReadFile("testdata/bad_signature_preeof1.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, VerifyResultUnknown, res.Result)
|
||||
assert.Contains(t, res.Message, "end-of-file")
|
||||
}
|
||||
|
||||
// Bad case: Incomplete signature data #2
|
||||
{
|
||||
c, err := os.ReadFile("testdata/bad_signature_preeof2.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, VerifyResultUnknown, res.Result)
|
||||
assert.Contains(t, res.Message, "end-of-file")
|
||||
}
|
||||
|
||||
// Bad case: No signature data #1
|
||||
{
|
||||
c, err := os.ReadFile("testdata/bad_signature_nodata.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, VerifyResultUnknown, res.Result)
|
||||
assert.Contains(t, res.Message, "no verification data found")
|
||||
}
|
||||
|
||||
// Bad case: Malformed signature data #1
|
||||
{
|
||||
c, err := os.ReadFile("testdata/bad_signature_malformed1.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, VerifyResultUnknown, res.Result)
|
||||
assert.Contains(t, res.Message, "no verification data found")
|
||||
}
|
||||
|
||||
// Bad case: Malformed signature data #2
|
||||
{
|
||||
c, err := os.ReadFile("testdata/bad_signature_malformed2.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, VerifyResultUnknown, res.Result)
|
||||
assert.Contains(t, res.Message, "Could not parse key ID")
|
||||
}
|
||||
|
||||
// Bad case: Malformed signature data #3
|
||||
{
|
||||
c, err := os.ReadFile("testdata/bad_signature_malformed3.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, VerifyResultUnknown, res.Result)
|
||||
assert.Contains(t, res.Message, "Could not parse result of verify")
|
||||
}
|
||||
|
||||
// Bad case: Invalid key ID in signature
|
||||
{
|
||||
c, err := os.ReadFile("testdata/bad_signature_badkeyid.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res := ParseGitCommitVerification(string(c))
|
||||
assert.Equal(t, VerifyResultUnknown, res.Result)
|
||||
assert.Contains(t, res.Message, "Invalid PGP key ID")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GetGnuPGHomePath(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
t.Setenv(common.EnvGnuPGHome, "")
|
||||
|
|
@ -468,30 +296,35 @@ func Test_GetGnuPGHomePath(t *testing.T) {
|
|||
func Test_KeyID(t *testing.T) {
|
||||
// Good case - long key ID (aka fingerprint) to short key ID
|
||||
{
|
||||
res := KeyID(longKeyID)
|
||||
res, err := KeyID(longKeyID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, shortKeyID, res)
|
||||
}
|
||||
// Good case - short key ID remains same
|
||||
{
|
||||
res := KeyID(shortKeyID)
|
||||
res, err := KeyID(shortKeyID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, shortKeyID, res)
|
||||
}
|
||||
// Bad case - key ID too short
|
||||
{
|
||||
keyID := "AEE18F83AFDEB23"
|
||||
res := KeyID(keyID)
|
||||
res, err := KeyID(keyID)
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
// Bad case - key ID too long
|
||||
{
|
||||
keyID := "5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB2323"
|
||||
res := KeyID(keyID)
|
||||
res, err := KeyID(keyID)
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
// Bad case - right length, but not hex string
|
||||
{
|
||||
keyID := "abcdefghijklmn"
|
||||
res := KeyID(keyID)
|
||||
res, err := KeyID(keyID)
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
}
|
||||
235
util/sourceintegrity/source_integrity.go
Normal file
235
util/sourceintegrity/source_integrity.go
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
package sourceintegrity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/git"
|
||||
"github.com/argoproj/argo-cd/v3/util/glob"
|
||||
)
|
||||
|
||||
type gitFunc func(gitClient git.Client, verifiedRevision string) (result *v1alpha1.SourceIntegrityCheckResult, legacyDescription string, err error)
|
||||
|
||||
var _gpgDisabledLoggedAlready bool
|
||||
|
||||
// HasCriteria determines if any of the sources have some criteria declared
|
||||
func HasCriteria(si *v1alpha1.SourceIntegrity, sources ...v1alpha1.ApplicationSource) bool {
|
||||
if si == nil || si.Git == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
if !source.IsZero() && !source.IsOCI() && !source.IsHelm() {
|
||||
if lookupGit(si, source.RepoURL) != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// VerifyGit makes sure the git repository satisfies the criteria declared.
|
||||
// It returns nil in case there were no relevant criteria, a check result if there were.
|
||||
// The verifiedRevision is expected to be either an annotated tag to a resolved commit sha - the revision, its signature is being verified.
|
||||
func VerifyGit(si *v1alpha1.SourceIntegrity, gitClient git.Client, verifiedRevision string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
|
||||
if si == nil || si.Git == nil {
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
check := lookupGit(si, gitClient.RepoURL())
|
||||
if check != nil {
|
||||
return check(gitClient, verifiedRevision)
|
||||
}
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
func lookupGit(si *v1alpha1.SourceIntegrity, repoURL string) gitFunc {
|
||||
policies := findMatchingGitPolicies(si.Git, repoURL)
|
||||
nPolicies := len(policies)
|
||||
if nPolicies == 0 {
|
||||
log.Infof("No git source integrity policies found for repo URL: %s", repoURL)
|
||||
return nil
|
||||
}
|
||||
if nPolicies > 1 {
|
||||
// Multiple matching policies is an error. BUT, it has to return a check that fails for every repo.
|
||||
// This is to make sure that a mistake in argo cd configuration does not disable verification until fixed.
|
||||
msg := fmt.Sprintf("multiple (%d) git source integrity policies found for repo URL: %s", nPolicies, repoURL)
|
||||
log.Warn(msg)
|
||||
return func(_ git.Client, _ string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
|
||||
return nil, "", errors.New(msg)
|
||||
}
|
||||
}
|
||||
|
||||
policy := policies[0]
|
||||
if policy.GPG != nil {
|
||||
if policy.GPG.Mode == v1alpha1.SourceIntegrityGitPolicyGPGModeNone {
|
||||
// Declare missing check because there is no verification performed
|
||||
return nil
|
||||
}
|
||||
|
||||
if !_gpgDisabledLoggedAlready && !IsGPGEnabled() {
|
||||
log.Warnf("SourceIntegrity criteria for git+gpg declared, but it is turned off by ARGOCD_GPG_ENABLED")
|
||||
_gpgDisabledLoggedAlready = true
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(gitClient git.Client, verifiedRevision string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
|
||||
return verify(policy.GPG, gitClient, verifiedRevision)
|
||||
}
|
||||
}
|
||||
|
||||
log.Warnf("No verification configured for SourceIntegrity policy for %+v", policy.Repos)
|
||||
return nil
|
||||
}
|
||||
|
||||
func findMatchingGitPolicies(si *v1alpha1.SourceIntegrityGit, repoURL string) (policies []*v1alpha1.SourceIntegrityGitPolicy) {
|
||||
for _, p := range si.Policies {
|
||||
include := false
|
||||
for _, r := range p.Repos {
|
||||
m := repoMatches(r.URL, repoURL)
|
||||
if m == -1 {
|
||||
include = false
|
||||
break
|
||||
} else if m == 1 {
|
||||
include = true
|
||||
}
|
||||
}
|
||||
if include {
|
||||
policies = append(policies, p)
|
||||
}
|
||||
}
|
||||
return policies
|
||||
}
|
||||
|
||||
func repoMatches(urlGlob string, repoURL string) int {
|
||||
if strings.HasPrefix(urlGlob, "!") {
|
||||
if glob.Match(urlGlob[1:], repoURL) {
|
||||
return -1
|
||||
}
|
||||
} else {
|
||||
if glob.Match(urlGlob, repoURL) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func verify(g *v1alpha1.SourceIntegrityGitPolicyGPG, gitClient git.Client, verifiedRevision string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
|
||||
const checkName = "GIT/GPG"
|
||||
|
||||
var deep bool
|
||||
switch g.Mode {
|
||||
// verify tag if on tag, latest revision otherwise
|
||||
case v1alpha1.SourceIntegrityGitPolicyGPGModeHead:
|
||||
deep = false
|
||||
// verify history from the current commit
|
||||
case v1alpha1.SourceIntegrityGitPolicyGPGModeStrict:
|
||||
deep = true
|
||||
default:
|
||||
return nil, "", fmt.Errorf("unknown GPG mode %q configured for GIT source integrity", g.Mode)
|
||||
}
|
||||
|
||||
signatures, err := gitClient.LsSignatures(verifiedRevision, deep)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if len(signatures) == 0 {
|
||||
panic("no signatures found for " + verifiedRevision)
|
||||
}
|
||||
|
||||
problems, legacyDescription := describeProblems(g, signatures)
|
||||
result := &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
|
||||
Name: checkName,
|
||||
Problems: problems,
|
||||
}}}
|
||||
return result, legacyDescription, nil
|
||||
}
|
||||
|
||||
// describeProblems reports 10 most recent problematic signatures or unsigned commits.
|
||||
// The number is limited not to flood the UI and logs with too many problems. Problems related to the same signing key are squashed.
|
||||
func describeProblems(g *v1alpha1.SourceIntegrityGitPolicyGPG, signatureInfos []git.RevisionSignatureInfo) (problems []string, legacyDescription string) {
|
||||
reportedKeys := make(map[string]any)
|
||||
for _, signatureInfo := range signatureInfos {
|
||||
// TODO: Delete with next major version. Backward compatibility only
|
||||
if legacyDescription == "" {
|
||||
if signatureInfo.VerificationResult == git.GPGVerificationResultUnsigned {
|
||||
legacyDescription = "Revision is not signed."
|
||||
} else {
|
||||
legacyResult := map[git.GPGVerificationResult]string{
|
||||
git.GPGVerificationResultGood: "Good",
|
||||
git.GPGVerificationResultBad: "Bad",
|
||||
git.GPGVerificationResultUntrusted: "Invalid",
|
||||
git.GPGVerificationResultExpiredSignature: "Invalid",
|
||||
git.GPGVerificationResultExpiredKey: "Invalid",
|
||||
git.GPGVerificationResultRevokedKey: "Invalid",
|
||||
git.GPGVerificationResultMissingKey: "Invalid",
|
||||
}[signatureInfo.VerificationResult]
|
||||
legacyDescription = fmt.Sprintf("%s signature from %s key %s", legacyResult, signatureInfo.AuthorIdentity, signatureInfo.SignatureKeyID)
|
||||
}
|
||||
}
|
||||
|
||||
// Do not report the same key twice unless:
|
||||
// - the revision is unsigned (unsigned commits can have different authors, so they are all worth reporting)
|
||||
// - the revision is a tag (tags are signed separately from commits)
|
||||
if signatureInfo.SignatureKeyID != "" && git.IsCommitSHA(signatureInfo.Revision) {
|
||||
if _, exists := reportedKeys[signatureInfo.SignatureKeyID]; exists {
|
||||
continue
|
||||
}
|
||||
reportedKeys[signatureInfo.SignatureKeyID] = nil
|
||||
}
|
||||
|
||||
problem := gpgProblemMessage(g, signatureInfo)
|
||||
if problem != "" {
|
||||
problems = append(problems, problem)
|
||||
|
||||
// Report at most 10 problems
|
||||
if len(problems) >= 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems, legacyDescription
|
||||
}
|
||||
|
||||
// gpgProblemMessage generates a message describing GPG verification issues for a specific revision signature and the configured policy.
|
||||
// When an empty string is returned, it means there is no problem - the validation has passed.
|
||||
func gpgProblemMessage(g *v1alpha1.SourceIntegrityGitPolicyGPG, signatureInfo git.RevisionSignatureInfo) string {
|
||||
if signatureInfo.VerificationResult != git.GPGVerificationResultGood {
|
||||
return fmt.Sprintf(
|
||||
"Failed verifying revision %s by '%s': %s (key_id=%s)",
|
||||
signatureInfo.Revision, signatureInfo.AuthorIdentity, signatureInfo.VerificationResult, signatureInfo.SignatureKeyID,
|
||||
)
|
||||
}
|
||||
|
||||
for _, allowedKey := range g.Keys {
|
||||
allowedKey, err := KeyID(allowedKey)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
if allowedKey == signatureInfo.SignatureKeyID {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"Failed verifying revision %s by '%s': signed with unallowed key (key_id=%s)",
|
||||
signatureInfo.Revision, signatureInfo.AuthorIdentity, signatureInfo.SignatureKeyID,
|
||||
)
|
||||
}
|
||||
|
||||
// IsGPGEnabled returns true if the GPG feature is enabled
|
||||
func IsGPGEnabled() bool {
|
||||
if en := os.Getenv("ARGOCD_GPG_ENABLED"); strings.EqualFold(en, "false") || strings.EqualFold(en, "no") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
535
util/sourceintegrity/source_integrity_test.go
Normal file
535
util/sourceintegrity/source_integrity_test.go
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
package sourceintegrity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/git"
|
||||
gitmocks "github.com/argoproj/argo-cd/v3/util/git/mocks"
|
||||
utilTest "github.com/argoproj/argo-cd/v3/util/test"
|
||||
)
|
||||
|
||||
func Test_IsGPGEnabled(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
assert.True(t, IsGPGEnabled())
|
||||
})
|
||||
|
||||
t.Run("false", func(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "false")
|
||||
assert.False(t, IsGPGEnabled())
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "")
|
||||
assert.True(t, IsGPGEnabled())
|
||||
})
|
||||
}
|
||||
|
||||
func Test_GPGDisabledLogging(t *testing.T) {
|
||||
t.Setenv("ARGOCD_GPG_ENABLED", "false")
|
||||
|
||||
si := &v1alpha1.SourceIntegrity{Git: &v1alpha1.SourceIntegrityGit{Policies: []*v1alpha1.SourceIntegrityGitPolicy{{
|
||||
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{{URL: "*"}},
|
||||
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
|
||||
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeStrict,
|
||||
Keys: []string{"SOME_KEY_ID"},
|
||||
},
|
||||
}}}}
|
||||
|
||||
logger := utilTest.LogHook{}
|
||||
logrus.AddHook(&logger)
|
||||
t.Cleanup(logger.CleanupHook)
|
||||
|
||||
fun := lookupGit(si, "https://github.com/argoproj/argo-cd.git")
|
||||
assert.Equal(t, []string{"SourceIntegrity criteria for git+gpg declared, but it is turned off by ARGOCD_GPG_ENABLED"}, logger.GetEntries())
|
||||
assert.Nil(t, fun)
|
||||
|
||||
// No logs on the second call
|
||||
logger.Entries = []logrus.Entry{}
|
||||
lookupGit(si, "https://github.com/argoproj/argo-cd-ext.git")
|
||||
assert.Equal(t, []string{}, logger.GetEntries())
|
||||
assert.Nil(t, fun)
|
||||
}
|
||||
|
||||
func TestGPGUnknownMode(t *testing.T) {
|
||||
gitClient := &gitmocks.Client{}
|
||||
gitClient.EXPECT().IsAnnotatedTag(mock.Anything).Return(false)
|
||||
gitClient.EXPECT().CommitSHA().Return("DEADBEEF", nil)
|
||||
|
||||
s := &v1alpha1.SourceIntegrityGitPolicyGPG{Mode: "foobar", Keys: []string{}}
|
||||
result, _, err := verify(s, gitClient, "https://github.com/argoproj/argo-cd.git")
|
||||
require.ErrorContains(t, err, `unknown GPG mode "foobar" configured for GIT source integrity`)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestNullOrEmptyDoesNothing(t *testing.T) {
|
||||
repoURL := "https://github.com/argoproj/argo-cd"
|
||||
applicationSource := v1alpha1.ApplicationSource{RepoURL: repoURL}
|
||||
|
||||
gitClient := &gitmocks.Client{}
|
||||
gitClient.EXPECT().RepoURL().Return(repoURL)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
si *v1alpha1.SourceIntegrity
|
||||
logged []string
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
si: nil,
|
||||
logged: []string{},
|
||||
},
|
||||
{
|
||||
name: "No GIT",
|
||||
si: &v1alpha1.SourceIntegrity{}, // No Git or alternative specified
|
||||
logged: []string{},
|
||||
},
|
||||
{
|
||||
name: "No matching policy",
|
||||
si: &v1alpha1.SourceIntegrity{Git: &v1alpha1.SourceIntegrityGit{}}, // No policies configured here
|
||||
logged: []string{},
|
||||
},
|
||||
{
|
||||
name: "Matching policy does nothing",
|
||||
si: &v1alpha1.SourceIntegrity{Git: &v1alpha1.SourceIntegrityGit{Policies: []*v1alpha1.SourceIntegrityGitPolicy{{
|
||||
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{{URL: "*"}},
|
||||
// No GPG or alternative specified
|
||||
}}}},
|
||||
logged: []string{"No verification configured for SourceIntegrity policy for [{URL:*}]"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger := utilTest.LogHook{}
|
||||
logrus.AddHook(&logger)
|
||||
t.Cleanup(logger.CleanupHook)
|
||||
|
||||
assert.False(t, HasCriteria(tt.si, applicationSource))
|
||||
assert.Equal(t, tt.logged, logger.GetEntries())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyMatching(t *testing.T) {
|
||||
group := &v1alpha1.SourceIntegrityGitPolicy{
|
||||
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{
|
||||
{URL: "https://github.com/group/*"},
|
||||
{URL: "!https://github.com/group/*-legacy.git"},
|
||||
{URL: "!https://github.com/group/*-critical.git"},
|
||||
},
|
||||
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
|
||||
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeHead,
|
||||
},
|
||||
}
|
||||
legacy := &v1alpha1.SourceIntegrityGitPolicy{
|
||||
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{
|
||||
{URL: "https://github.com/group/*-legacy.git"},
|
||||
},
|
||||
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
|
||||
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeNone,
|
||||
},
|
||||
}
|
||||
critical := &v1alpha1.SourceIntegrityGitPolicy{
|
||||
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{
|
||||
{URL: "https://github.com/group/*-critical.git"},
|
||||
},
|
||||
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
|
||||
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeStrict,
|
||||
},
|
||||
}
|
||||
// collides with group
|
||||
duplicated := &v1alpha1.SourceIntegrityGitPolicy{
|
||||
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{
|
||||
{URL: "https://github.com/group/duplicated.git"},
|
||||
},
|
||||
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
|
||||
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeHead,
|
||||
},
|
||||
}
|
||||
sig := &v1alpha1.SourceIntegrityGit{
|
||||
Policies: []*v1alpha1.SourceIntegrityGitPolicy{group, legacy, critical, duplicated},
|
||||
}
|
||||
|
||||
p := func(ps ...*v1alpha1.SourceIntegrityGitPolicy) []*v1alpha1.SourceIntegrityGitPolicy { return ps }
|
||||
testCases := []struct {
|
||||
repo string
|
||||
expectedPolicies []*v1alpha1.SourceIntegrityGitPolicy
|
||||
expectedLogs []string
|
||||
expectedNoFunc bool
|
||||
}{
|
||||
{
|
||||
repo: "https://github.com/group/head.git",
|
||||
expectedPolicies: p(group),
|
||||
expectedLogs: []string{},
|
||||
},
|
||||
{
|
||||
repo: "https://github.com/group/foo-legacy.git",
|
||||
expectedPolicies: p(legacy),
|
||||
expectedLogs: []string{},
|
||||
expectedNoFunc: true, // The mode is "none"
|
||||
},
|
||||
{
|
||||
repo: "https://github.com/group/bar-critical.git",
|
||||
expectedPolicies: p(critical),
|
||||
expectedLogs: []string{},
|
||||
},
|
||||
{
|
||||
repo: "https://github.com/group/duplicated.git",
|
||||
expectedPolicies: p(group, duplicated),
|
||||
expectedLogs: []string{"multiple (2) git source integrity policies found for repo URL: https://github.com/group/duplicated.git"},
|
||||
},
|
||||
{
|
||||
repo: "https://gitlab.com/foo/bar.git",
|
||||
expectedPolicies: p(),
|
||||
expectedLogs: []string{"No git source integrity policies found for repo URL: https://gitlab.com/foo/bar.git"},
|
||||
expectedNoFunc: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.repo, func(t *testing.T) {
|
||||
actual := findMatchingGitPolicies(sig, tt.repo)
|
||||
|
||||
assert.Equal(t, tt.expectedPolicies, actual)
|
||||
|
||||
hook := utilTest.NewLogHook(logrus.InfoLevel)
|
||||
logrus.AddHook(hook)
|
||||
defer hook.CleanupHook()
|
||||
si := &v1alpha1.SourceIntegrity{Git: sig}
|
||||
forGitFunc := lookupGit(si, tt.repo)
|
||||
if tt.expectedNoFunc {
|
||||
assert.Nil(t, forGitFunc)
|
||||
} else {
|
||||
assert.NotNil(t, forGitFunc)
|
||||
}
|
||||
assert.Equal(t, tt.expectedLogs, hook.GetEntries())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that when a user has configured the full fingerprint, it is still accepted
|
||||
func TestComparingWithGPGFingerprint(t *testing.T) {
|
||||
const shortKey = "D56C4FCA57A46444"
|
||||
const fingerprint = "01234567890123456789abcd" + shortKey
|
||||
require.True(t, IsShortKeyID(shortKey))
|
||||
require.True(t, IsLongKeyID(fingerprint))
|
||||
|
||||
gitClient := &gitmocks.Client{}
|
||||
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).Return([]git.RevisionSignatureInfo{{
|
||||
Revision: "1.0", VerificationResult: git.GPGVerificationResultGood, SignatureKeyID: shortKey, Date: "ignored", AuthorIdentity: "ignored",
|
||||
}}, nil)
|
||||
|
||||
gpgWithTag := &v1alpha1.SourceIntegrityGitPolicyGPG{Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeHead, Keys: []string{fingerprint}}
|
||||
// And verifying a given revision
|
||||
result, legacy, err := verify(gpgWithTag, gitClient, "1.0")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, result.IsValid())
|
||||
require.NoError(t, result.AsError())
|
||||
assert.Equal(t, "Good signature from ignored key D56C4FCA57A46444", legacy)
|
||||
}
|
||||
|
||||
func TestGPGHeadValid(t *testing.T) {
|
||||
const sha = "0c7a9c3f939c1f19b518bcdd11e2fce9703c4901"
|
||||
const tag = "tag"
|
||||
const keyId = "4cfe068f80b1681b"
|
||||
testCases := []struct {
|
||||
revision string
|
||||
check func(gitClient *gitmocks.Client, logger utilTest.LogHook)
|
||||
}{
|
||||
{
|
||||
revision: sha,
|
||||
check: func(gitClient *gitmocks.Client, logger utilTest.LogHook) {
|
||||
gitClient.AssertCalled(t, "LsSignatures", sha, false)
|
||||
assert.Empty(t, logger.GetEntries())
|
||||
},
|
||||
},
|
||||
{
|
||||
revision: tag,
|
||||
check: func(gitClient *gitmocks.Client, logger utilTest.LogHook) {
|
||||
gitClient.AssertCalled(t, "LsSignatures", tag, false)
|
||||
assert.Empty(t, logger.GetEntries())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run("verify "+test.revision, func(t *testing.T) {
|
||||
// Given repo with a tagged commit
|
||||
gitClient := &gitmocks.Client{}
|
||||
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(func(revision string, _ bool) ([]git.RevisionSignatureInfo, error) {
|
||||
return []git.RevisionSignatureInfo{{
|
||||
Revision: revision, VerificationResult: git.GPGVerificationResultGood, SignatureKeyID: keyId, Date: "ignored", AuthorIdentity: "ignored",
|
||||
}}, nil
|
||||
})
|
||||
|
||||
logger := utilTest.LogHook{}
|
||||
logrus.AddHook(&logger)
|
||||
t.Cleanup(logger.CleanupHook)
|
||||
|
||||
// When using head mode
|
||||
gpgWithTag := &v1alpha1.SourceIntegrityGitPolicyGPG{
|
||||
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeHead,
|
||||
Keys: []string{keyId, "0000000000000000"},
|
||||
}
|
||||
// And verifying a given revision
|
||||
result, legacy, err := verify(gpgWithTag, gitClient, test.revision)
|
||||
require.NoError(t, err)
|
||||
// Then it is checked and valid
|
||||
assert.True(t, result.IsValid())
|
||||
assert.Equal(t, []string{"GIT/GPG"}, result.PassedChecks())
|
||||
test.check(gitClient, logger)
|
||||
require.NoError(t, result.AsError())
|
||||
assert.Equal(t, "Good signature from ignored key 4cfe068f80b1681b", legacy)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeProblems(t *testing.T) {
|
||||
const r = "aafc9e88599f24802b113b6278e42eaadda32cd6"
|
||||
const a = "Commit Author <nereply@acme.com>"
|
||||
const kGood = "AAAAAAAAAAAAAAAA"
|
||||
const kOk = "BBBBBBBBBBBBBBB"
|
||||
policy := v1alpha1.SourceIntegrityGitPolicyGPG{Keys: []string{kGood, kOk}}
|
||||
|
||||
sig := func(key string, result git.GPGVerificationResult) git.RevisionSignatureInfo {
|
||||
return git.RevisionSignatureInfo{
|
||||
Revision: r,
|
||||
VerificationResult: result,
|
||||
SignatureKeyID: key,
|
||||
AuthorIdentity: a,
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
gpg *v1alpha1.SourceIntegrityGitPolicyGPG
|
||||
sigs []git.RevisionSignatureInfo
|
||||
expected []string
|
||||
legacy string
|
||||
}{
|
||||
{
|
||||
name: "report only problems",
|
||||
gpg: &policy,
|
||||
sigs: []git.RevisionSignatureInfo{
|
||||
sig("bad", git.GPGVerificationResultRevokedKey),
|
||||
sig(kGood, git.GPGVerificationResultGood),
|
||||
sig("also_bad", git.GPGVerificationResultUntrusted),
|
||||
},
|
||||
expected: []string{
|
||||
"Failed verifying revision " + r + " by '" + a + "': signed with revoked key (key_id=bad)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': signed with untrusted key (key_id=also_bad)",
|
||||
},
|
||||
legacy: "Invalid signature from Commit Author <nereply@acme.com> key bad",
|
||||
},
|
||||
{
|
||||
name: "collapse problems of the same key",
|
||||
gpg: &policy,
|
||||
sigs: []git.RevisionSignatureInfo{
|
||||
sig("bad", git.GPGVerificationResultRevokedKey),
|
||||
sig(kGood, git.GPGVerificationResultGood),
|
||||
sig("also_bad", git.GPGVerificationResultUntrusted),
|
||||
sig("bad", git.GPGVerificationResultRevokedKey),
|
||||
},
|
||||
expected: []string{
|
||||
"Failed verifying revision " + r + " by '" + a + "': signed with revoked key (key_id=bad)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': signed with untrusted key (key_id=also_bad)",
|
||||
},
|
||||
legacy: "Invalid signature from Commit Author <nereply@acme.com> key bad",
|
||||
},
|
||||
{
|
||||
name: "do not collapse unsigned commits, as they can differ by author",
|
||||
gpg: &policy,
|
||||
sigs: []git.RevisionSignatureInfo{
|
||||
sig("", git.GPGVerificationResultUnsigned),
|
||||
sig("", git.GPGVerificationResultUnsigned),
|
||||
sig("", git.GPGVerificationResultUnsigned),
|
||||
},
|
||||
expected: []string{
|
||||
"Failed verifying revision " + r + " by '" + a + "': unsigned (key_id=)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': unsigned (key_id=)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': unsigned (key_id=)",
|
||||
},
|
||||
legacy: "Revision is not signed.",
|
||||
},
|
||||
{
|
||||
name: "Report first ten problems only",
|
||||
gpg: &policy,
|
||||
sigs: []git.RevisionSignatureInfo{
|
||||
sig("revoked", git.GPGVerificationResultRevokedKey),
|
||||
sig("", git.GPGVerificationResultUnsigned),
|
||||
sig("untrusted", git.GPGVerificationResultUntrusted),
|
||||
sig("missing", git.GPGVerificationResultMissingKey),
|
||||
sig("expired_key", git.GPGVerificationResultExpiredKey),
|
||||
sig("expired_sig", git.GPGVerificationResultExpiredSignature),
|
||||
sig("bad", git.GPGVerificationResultBad),
|
||||
sig("also_bad", git.GPGVerificationResultBad),
|
||||
sig("more_bad", git.GPGVerificationResultBad),
|
||||
sig("outright_terrible", git.GPGVerificationResultBad),
|
||||
// the rest is cut off
|
||||
sig("OMG", git.GPGVerificationResultBad),
|
||||
sig("nope", git.GPGVerificationResultBad),
|
||||
sig("you_gotta_be_kidding_me", git.GPGVerificationResultBad),
|
||||
},
|
||||
expected: []string{
|
||||
"Failed verifying revision " + r + " by '" + a + "': signed with revoked key (key_id=revoked)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': unsigned (key_id=)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': signed with untrusted key (key_id=untrusted)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': signed with key not in keyring (key_id=missing)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': signed with expired key (key_id=expired_key)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': expired signature (key_id=expired_sig)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': bad signature (key_id=bad)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': bad signature (key_id=also_bad)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': bad signature (key_id=more_bad)",
|
||||
"Failed verifying revision " + r + " by '" + a + "': bad signature (key_id=outright_terrible)",
|
||||
},
|
||||
legacy: "Invalid signature from Commit Author <nereply@acme.com> key revoked",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
problems, legacy := describeProblems(tt.gpg, tt.sigs)
|
||||
assert.Equal(t, tt.expected, problems)
|
||||
assert.Equal(t, tt.legacy, legacy)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGPGStrictValid(t *testing.T) {
|
||||
const shaFirst = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
const shaSecond = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
const shaThird = "cccccccccccccccccccccccccccccccccccccccc"
|
||||
const tagFirst = "tag-first"
|
||||
const tagSecond = "tag-second"
|
||||
const tagThird = "tag-third"
|
||||
|
||||
const keyOfThird = "9c698b961c1088db"
|
||||
const keyOfSecond = "f4b9db205449e1d9"
|
||||
const keyOfFirst = "92bfcec2e8161558"
|
||||
|
||||
rsi := func(rev string, key string) git.RevisionSignatureInfo {
|
||||
return git.RevisionSignatureInfo{
|
||||
Revision: rev,
|
||||
VerificationResult: git.GPGVerificationResultGood,
|
||||
SignatureKeyID: key,
|
||||
Date: "ignored",
|
||||
AuthorIdentity: "ignored",
|
||||
}
|
||||
}
|
||||
// To be resolved as lsSignatures[deep][revision]
|
||||
lsSignatures := map[bool]map[string][]git.RevisionSignatureInfo{
|
||||
// Return info for all preceding revisions. If revision is a tag, start with a tag.
|
||||
true: {
|
||||
shaFirst: []git.RevisionSignatureInfo{rsi(shaFirst, keyOfFirst)},
|
||||
tagFirst: []git.RevisionSignatureInfo{rsi(tagFirst, keyOfFirst), rsi(tagFirst, keyOfFirst)},
|
||||
shaSecond: []git.RevisionSignatureInfo{rsi(shaSecond, keyOfSecond), rsi(shaFirst, keyOfFirst)},
|
||||
tagSecond: []git.RevisionSignatureInfo{rsi(tagSecond, keyOfSecond), rsi(shaSecond, keyOfSecond), rsi(shaFirst, keyOfFirst)},
|
||||
shaThird: []git.RevisionSignatureInfo{rsi(shaThird, keyOfThird), rsi(shaSecond, keyOfSecond), rsi(shaFirst, keyOfFirst)},
|
||||
tagThird: []git.RevisionSignatureInfo{rsi(tagThird, keyOfThird), rsi(shaThird, keyOfThird), rsi(shaSecond, keyOfSecond), rsi(shaFirst, keyOfFirst)},
|
||||
},
|
||||
// Return info for just the tag or revision
|
||||
false: {
|
||||
shaFirst: []git.RevisionSignatureInfo{rsi(shaFirst, keyOfFirst)},
|
||||
tagFirst: []git.RevisionSignatureInfo{rsi(tagFirst, keyOfFirst)},
|
||||
shaSecond: []git.RevisionSignatureInfo{rsi(shaSecond, keyOfSecond)},
|
||||
tagSecond: []git.RevisionSignatureInfo{rsi(tagSecond, keyOfSecond)},
|
||||
shaThird: []git.RevisionSignatureInfo{rsi(shaThird, keyOfThird)},
|
||||
tagThird: []git.RevisionSignatureInfo{rsi(tagThird, keyOfThird)},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
revision string
|
||||
expectedErr string
|
||||
expectedPassed []string
|
||||
expectedLsArgs []any
|
||||
}{
|
||||
{
|
||||
revision: shaFirst,
|
||||
expectedPassed: []string{"GIT/GPG"},
|
||||
expectedLsArgs: []any{shaFirst, true},
|
||||
},
|
||||
{
|
||||
revision: shaSecond,
|
||||
expectedPassed: []string{"GIT/GPG"},
|
||||
expectedLsArgs: []any{shaSecond, true},
|
||||
},
|
||||
{
|
||||
revision: shaThird,
|
||||
expectedPassed: []string{},
|
||||
expectedErr: fmt.Sprintf("GIT/GPG: Failed verifying revision %s by 'ignored': signed with unallowed key (key_id=%s)", shaThird, keyOfThird),
|
||||
expectedLsArgs: []any{shaThird, true},
|
||||
},
|
||||
{
|
||||
revision: tagFirst,
|
||||
expectedPassed: []string{"GIT/GPG"},
|
||||
expectedLsArgs: []any{shaFirst, true},
|
||||
},
|
||||
{
|
||||
revision: tagSecond,
|
||||
expectedPassed: []string{"GIT/GPG"},
|
||||
expectedLsArgs: []any{shaSecond, true},
|
||||
},
|
||||
{
|
||||
revision: tagThird,
|
||||
expectedPassed: []string{},
|
||||
expectedErr: fmt.Sprintf(`GIT/GPG: Failed verifying revision %s by 'ignored': signed with unallowed key (key_id=%s)
|
||||
GIT/GPG: Failed verifying revision %s by 'ignored': signed with unallowed key (key_id=%s)`, tagThird, keyOfThird, shaThird, keyOfThird),
|
||||
expectedLsArgs: []any{shaThird, true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("verify "+test.revision, func(t *testing.T) {
|
||||
// Given repo with a tagged commit
|
||||
gitClient := &gitmocks.Client{}
|
||||
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(
|
||||
func(revision string, deep bool) (info []git.RevisionSignatureInfo, err error) {
|
||||
if ret, ok := lsSignatures[deep][revision]; ok {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
panic("unknown revision " + revision)
|
||||
},
|
||||
)
|
||||
|
||||
logger := utilTest.LogHook{}
|
||||
logrus.AddHook(&logger)
|
||||
t.Cleanup(logger.CleanupHook)
|
||||
|
||||
// When using strict mode
|
||||
gpgWithTag := &v1alpha1.SourceIntegrityGitPolicyGPG{
|
||||
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeStrict,
|
||||
Keys: []string{keyOfFirst, keyOfSecond},
|
||||
}
|
||||
// And verifying a given revision
|
||||
result, legacy, err := verify(gpgWithTag, gitClient, test.revision)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then it is checked and valid
|
||||
err = result.AsError()
|
||||
if test.expectedErr == "" {
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.IsValid())
|
||||
assert.Contains(t, legacy, "Good signature from ")
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, test.expectedErr, err.Error())
|
||||
assert.False(t, result.IsValid())
|
||||
// Confusing but correct. Signature is good, but not allowed in project so this should be rejected.
|
||||
assert.Contains(t, legacy, "Good signature from ")
|
||||
}
|
||||
assert.Equal(t, test.expectedPassed, result.PassedChecks())
|
||||
assert.Empty(t, logger.GetEntries())
|
||||
})
|
||||
}
|
||||
}
|
||||
23
util/sourceintegrity/testdata/data.go
vendored
Normal file
23
util/sourceintegrity/testdata/data.go
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package testdata
|
||||
|
||||
import _ "embed"
|
||||
|
||||
var (
|
||||
//go:embed garbage.asc
|
||||
Garbage_asc string
|
||||
|
||||
//go:embed github.asc
|
||||
Github_asc string
|
||||
|
||||
//go:embed janedoe.asc
|
||||
Janedoe_asc string
|
||||
|
||||
//go:embed johndoe.asc
|
||||
Johndoe_asc string
|
||||
|
||||
//go:embed multi.asc
|
||||
Multi_asc string
|
||||
|
||||
//go:embed multi2.asc
|
||||
Multi2_asc string
|
||||
)
|
||||
|
|
@ -292,11 +292,24 @@ func generateJWTToken(issuer string) (string, error) {
|
|||
}
|
||||
|
||||
type LogHook struct {
|
||||
Entries []log.Entry
|
||||
Entries []log.Entry
|
||||
minLevel log.Level
|
||||
}
|
||||
|
||||
func NewLogHook(minLevel log.Level) *LogHook {
|
||||
return &LogHook{minLevel: minLevel}
|
||||
}
|
||||
|
||||
func (h *LogHook) Levels() []log.Level {
|
||||
return []log.Level{log.WarnLevel}
|
||||
if h.minLevel == 0 {
|
||||
h.minLevel = log.WarnLevel
|
||||
}
|
||||
|
||||
var levels []log.Level
|
||||
for i := log.PanicLevel; i <= h.minLevel; i++ {
|
||||
levels = append(levels, i)
|
||||
}
|
||||
return levels
|
||||
}
|
||||
|
||||
func (h *LogHook) Fire(entry *log.Entry) error {
|
||||
|
|
@ -304,6 +317,10 @@ func (h *LogHook) Fire(entry *log.Entry) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (h *LogHook) CleanupHook() {
|
||||
log.StandardLogger().ReplaceHooks(log.LevelHooks{})
|
||||
}
|
||||
|
||||
func (h *LogHook) GetRegexMatchesInEntries(match string) []string {
|
||||
re := regexp.MustCompile(match)
|
||||
matches := make([]string, 0)
|
||||
|
|
@ -314,3 +331,11 @@ func (h *LogHook) GetRegexMatchesInEntries(match string) []string {
|
|||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func (h *LogHook) GetEntries() []string {
|
||||
matches := make([]string, 0)
|
||||
for _, entry := range h.Entries {
|
||||
matches = append(matches, entry.Message)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue