mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 08:57:17 +00:00
* Add initial primitives and tests for GPG related operations * More tests and test documentation * Move gpg primitives to own module * Add initial primitives for running git verify-commit and tests * Improve and better comment test * Implement VerifyCommitSignature() primitive for metrics wrapper * More commentary * Make reposerver verify gpg signatures when generating manifests * Make signature validation optional * Forbid use of local manifests when signature verification is enabled * Introduce new signatureKeys field in project CRD * Initial support for only syncing against signed revisions * Updates to GnuPG primitives and more test cases * Move signature verification to correct place and add tests * Add signature verification result to revision metadata and display it in UI * Add more primitives and move out some stuff to common module * Add more testdata * Add key management primitives to ArgoDB * Move type GnuPGPublicKey to appsv1 package * Add const ArgoCDGPGKeysConfigMapName * Handle key operations with appsv1.GnuPGPublicKey * Add initial API for managing GPG keys * Remove deprecated code * Add primitives for adding public keys to configuration * Change semantics of ValidateGPGKeys to return more key information * Add key import functionality to public key API * Fix code quirks reported by linter * More code quirks fixes * Fix test * Add primitives for deleting keys from configuration * Add delete key operation to API and CLI * Cosmetics * Implement logic to sync configuration to keyring in repo-server * Add IsGPGEnabled() primitive and also update trustdb on ownertrust changes * Use gpg.IsGPGEnabled() instead of custom test * Remove all keyring manipulating methods from DB * Cosmetics/comments * Require grpc methods from argoproj pkg * Enable setting config path via ARGOCD_GPG_DATA_PATH * Allow "no" and any cases in ARGOCD_GPG_ENABLED * Enable GPG feature on start and start-e2e and set required environment * Cosmetics/comments * Cosmetics and commentary * Update API documentation * Fix comment * Only run GPG related operations if GPG is enabled * Allow setting ARGOCD_GPG_ENABLE from the environment * Create GPG ConfigMap resource during installation * Use function instead of constant to get the watcher path * Re-watch source path in case it gets recreated. Also, error on finish * Add End-to-End tests for GPG commit verification * Introduce SignatureKey type for AppProject CRD * Fix merge error from previous commit * Adapt test for additional manifest (argocd-gpg-keys-cm.yaml) * Fix linter issues * Adapt CircleCI configuration to enable running tests * Add wrapper scripts for git and gpg * Sigh. * Display gpg version in CircleCI * Install gnupg2 and link it to gpg in CI * Try to install gnupg2 in CircleCI image * More CircleCI tweaks * # This is a combination of 10 commits. # This is the 1st commit message: Containerize tests - test cycle # This is the commit message #2: adapt working directory # This is the commit message #3: Build before running tests (so we might have a cache) # This is the commit message #4: Test limiting parallelism # This is the commit message #5: Remove unbound variable # This is the commit message #6: Decrease parallelism to find out limit # This is the commit message #7: Use correct flag # This is the commit message #8: Update Docker image # This is the commit message #9: Remove build phase and increase parallelism # This is the commit message #10: Further increase parallelism * Dockerize toolchain * Add new targets to Makefile * Codegen * Properly handle permissions for E2E tests * Remove gnupg2 installation from CircleCI configuration * Limit parallelism of build * Fix Yarn lint * Retrigger CI for possible flaky test * Codegen * Remove duplicate target in Makefile * Pull in pager from dep ensure -v * Adapt to gitops-engine changes and codegen * Use new health package for health status constants * Add GPG methods to ArgoDB mock module * Fix possible nil pointer dereference * Fix linter issue in imports * Introduce RBAC resource type 'gpgkeys' and adapt policies * Use ARGOCD_GNUPGHOME instead of GNUPGHOME for subsystem configuration Also remove some deprecated unit tests. * Also register GPG keys API with gRPC-GW * Update from codegen * Update GPG key API * Add web UI to manage GPG keys * Lint updates * Change wording * Add some plausibility checks for supplied data on key creation * Update from codegen * Re-allow binary keys and move check for ASCII armoured to UI * Make yarn lint happy * Add editing signature keys for projects in UI * Add ability to configure signature keys for project in CLI * Change default value to use for GNUPGHOME * Do not include data section in default gpg keys CM * Adapt Docker image for GnuPG feature * Add required configuration to installation manifests * Add add-signature-key and remove-signature-key commands to project CLI * Fix typo * Add initial user documentation for GnuPG verification * Fix role name - oops * Mention required RBAC roles in docs * Support GPG verification of git annotated tags as well * Ensure CLI can build succesfully * Better support verification on tags * Print key type in upper case * Update user documentation * Correctly disable GnuPG verification if ARGOCD_GPG_ENABLE=false * Clarify that this feature is only available with Git repositories * codegen * Move verification code to own function * Remove deprecated check * Make things more developer friendly when running locally * Enable GPG feature by default, and don't require ARGOCD_GNUPGHOME to be set * Revert changes to manifests to reflect default enable state * Codegen
This commit is contained in:
parent
a886241ef2
commit
be718e2b61
95 changed files with 7669 additions and 521 deletions
|
|
@ -50,12 +50,14 @@ RUN groupadd -g 999 argocd && \
|
|||
chmod g=u /home/argocd && \
|
||||
chmod g=u /etc/passwd && \
|
||||
apt-get update && \
|
||||
apt-get install -y git git-lfs python3-pip tini && \
|
||||
apt-get install -y git git-lfs python3-pip tini gpg && \
|
||||
apt-get clean && \
|
||||
pip3 install awscli==1.18.80 && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY hack/git-ask-pass.sh /usr/local/bin/git-ask-pass.sh
|
||||
COPY hack/gpg-wrapper.sh /usr/local/bin/gpg-wrapper.sh
|
||||
COPY hack/git-verify-wrapper.sh /usr/local/bin/git-verify-wrapper.sh
|
||||
COPY --from=builder /usr/local/bin/ks /usr/local/bin/ks
|
||||
COPY --from=builder /usr/local/bin/helm2 /usr/local/bin/helm2
|
||||
COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm
|
||||
|
|
@ -71,6 +73,10 @@ RUN mkdir -p /app/config/ssh && \
|
|||
ln -s /app/config/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts
|
||||
|
||||
RUN mkdir -p /app/config/tls
|
||||
RUN mkdir -p /app/config/gpg/source && \
|
||||
mkdir -p /app/config/gpg/keys && \
|
||||
chown argocd /app/config/gpg/keys && \
|
||||
chmod 0700 /app/config/gpg/keys
|
||||
|
||||
# workaround ksonnet issue https://github.com/ksonnet/ksonnet/issues/298
|
||||
ENV USER=argocd
|
||||
|
|
|
|||
19
Makefile
19
Makefile
|
|
@ -98,6 +98,8 @@ IMAGE_NAMESPACE?=
|
|||
STATIC_BUILD?=true
|
||||
# build development images
|
||||
DEV_IMAGE?=false
|
||||
ARGOCD_GPG_ENABLED?=true
|
||||
ARGOCD_E2E_APISERVER_PORT?=8080
|
||||
|
||||
override LDFLAGS += \
|
||||
-X ${PACKAGE}.version=${VERSION} \
|
||||
|
|
@ -159,6 +161,8 @@ codegen:
|
|||
|
||||
.PHONY: cli
|
||||
cli: clean-debug
|
||||
rm -f ${DIST_DIR}/${CLI_NAME}
|
||||
mkdir -p ${DIST_DIR}
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd/argocd
|
||||
|
||||
.PHONY: cli-docker
|
||||
|
|
@ -331,7 +335,7 @@ test-e2e:
|
|||
test-e2e-local: cli
|
||||
# NO_PROXY ensures all tests don't go out through a proxy if one is configured on the test system
|
||||
export GO111MODULE=off
|
||||
NO_PROXY=* ./hack/test.sh -timeout 15m -v ./test/e2e
|
||||
ARGOCD_GPG_ENABLED=true NO_PROXY=* ./hack/test.sh -timeout 15m -v ./test/e2e
|
||||
|
||||
# Spawns a shell in the test server container for debugging purposes
|
||||
debug-test-server:
|
||||
|
|
@ -354,9 +358,17 @@ start-e2e-local:
|
|||
kubectl create ns argocd-e2e || true
|
||||
kubectl config set-context --current --namespace=argocd-e2e
|
||||
kustomize build test/manifests/base | kubectl apply -f -
|
||||
# Create GPG keys and source directories
|
||||
if test -d /tmp/argo-e2e/app/config/gpg; then rm -rf /tmp/argo-e2e/app/config/gpg/*; fi
|
||||
mkdir -p /tmp/argo-e2e/app/config/gpg/keys && chmod 0700 /tmp/argo-e2e/app/config/gpg/keys
|
||||
mkdir -p /tmp/argo-e2e/app/config/gpg/source && chmod 0700 /tmp/argo-e2e/app/config/gpg/source
|
||||
if test "$(USER_ID)" != ""; then chown -R "$(USER_ID)" /tmp/argo-e2e; fi
|
||||
# set paths for locally managed ssh known hosts and tls certs data
|
||||
ARGOCD_SSH_DATA_PATH=/tmp/argo-e2e/app/config/ssh \
|
||||
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
|
||||
ARGOCD_GPG_DATA_PATH=/tmp/argo-e2e/app/config/gpg/source \
|
||||
ARGOCD_GNUPGHOME=/tmp/argo-e2e/app/config/gpg/keys \
|
||||
ARGOCD_GPG_ENABLED=true \
|
||||
ARGOCD_E2E_DISABLE_AUTH=false \
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
|
||||
|
|
@ -383,8 +395,13 @@ start-local: mod-vendor-local
|
|||
# check we can connect to Docker to start Redis
|
||||
killall goreman || true
|
||||
kubectl create ns argocd || true
|
||||
rm -rf /tmp/argocd-local
|
||||
mkdir -p /tmp/argocd-local
|
||||
mkdir -p /tmp/argocd-local/gpg/keys && chmod 0700 /tmp/argocd-local/gpg/keys
|
||||
mkdir -p /tmp/argocd-local/gpg/source
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
ARGOCD_IN_CI=false \
|
||||
ARGOCD_GPG_ENABLED=true \
|
||||
ARGOCD_E2E_TEST=false \
|
||||
goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START}
|
||||
|
||||
|
|
|
|||
8
Procfile
8
Procfile
|
|
@ -1,8 +1,8 @@
|
|||
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:=/tmp/argocd/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:=/tmp/argocd/ssh} go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
|
||||
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:=/tmp/argocd/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:=/tmp/argocd/ssh} go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --staticassets ui/dist/app"
|
||||
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
|
||||
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --staticassets ui/dist/app"
|
||||
dex: sh -c "go run github.com/argoproj/argo-cd/cmd/argocd-util gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.22.0 serve /dex.yaml"
|
||||
redis: docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} redis:5.0.8-alpine --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}
|
||||
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:=/tmp/argocd/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:=/tmp/argocd/ssh} go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
|
||||
git-server: test/fixture/testrepos/start-git.sh
|
||||
dev-mounter: [[ "$ARGOCD_E2E_TEST" != "true" ]] && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:=/tmp/argocd/tls}
|
||||
dev-mounter: [[ "$ARGOCD_E2E_TEST" != "true" ]] && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} --configmap argocd-gpg-keys-cm=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ p, role:readonly, clusters, get, *, allow
|
|||
p, role:readonly, repositories, get, *, allow
|
||||
p, role:readonly, projects, get, *, allow
|
||||
p, role:readonly, accounts, get, *, allow
|
||||
p, role:readonly, gpgkeys, get, *, allow
|
||||
|
||||
p, role:admin, applications, create, */*, allow
|
||||
p, role:admin, applications, update, */*, allow
|
||||
|
|
@ -32,6 +33,8 @@ p, role:admin, projects, create, *, allow
|
|||
p, role:admin, projects, update, *, allow
|
||||
p, role:admin, projects, delete, *, allow
|
||||
p, role:admin, accounts, update, *, allow
|
||||
p, role:admin, gpgkeys, create, *, allow
|
||||
p, role:admin, gpgkeys, delete, *, allow
|
||||
|
||||
g, role:admin, role:readonly
|
||||
g, admin, role:admin
|
||||
|
|
|
|||
|
|
|
@ -124,7 +124,7 @@
|
|||
"tags": [
|
||||
"AccountService"
|
||||
],
|
||||
"operationId": "CreateTokenMixin9",
|
||||
"operationId": "CreateTokenMixin10",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
|
@ -156,7 +156,7 @@
|
|||
"tags": [
|
||||
"AccountService"
|
||||
],
|
||||
"operationId": "DeleteTokenMixin9",
|
||||
"operationId": "DeleteTokenMixin10",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
"ApplicationService"
|
||||
],
|
||||
"summary": "List returns list of applications",
|
||||
"operationId": "ListMixin8",
|
||||
"operationId": "List",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
|
@ -237,7 +237,7 @@
|
|||
"ApplicationService"
|
||||
],
|
||||
"summary": "Create creates an application",
|
||||
"operationId": "CreateMixin8",
|
||||
"operationId": "Create",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
|
|
@ -264,7 +264,7 @@
|
|||
"ApplicationService"
|
||||
],
|
||||
"summary": "Update updates an application",
|
||||
"operationId": "UpdateMixin8",
|
||||
"operationId": "Update",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
|
@ -395,7 +395,7 @@
|
|||
"ApplicationService"
|
||||
],
|
||||
"summary": "Get returns an application by name",
|
||||
"operationId": "GetMixin8",
|
||||
"operationId": "GetMixin1",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
|
@ -445,7 +445,7 @@
|
|||
"ApplicationService"
|
||||
],
|
||||
"summary": "Delete deletes an application",
|
||||
"operationId": "DeleteMixin8",
|
||||
"operationId": "Delete",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
|
@ -1084,7 +1084,7 @@
|
|||
"ClusterService"
|
||||
],
|
||||
"summary": "List returns list of clusters",
|
||||
"operationId": "List",
|
||||
"operationId": "ListMixin5",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
|
@ -1111,7 +1111,7 @@
|
|||
"ClusterService"
|
||||
],
|
||||
"summary": "Create creates a cluster",
|
||||
"operationId": "Create",
|
||||
"operationId": "CreateMixin5",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
|
|
@ -1138,7 +1138,7 @@
|
|||
"ClusterService"
|
||||
],
|
||||
"summary": "Update updates a cluster",
|
||||
"operationId": "Update",
|
||||
"operationId": "UpdateMixin5",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
|
@ -1171,7 +1171,7 @@
|
|||
"ClusterService"
|
||||
],
|
||||
"summary": "Get returns a cluster by server address",
|
||||
"operationId": "GetMixin2",
|
||||
"operationId": "GetMixin5",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
|
@ -1199,7 +1199,7 @@
|
|||
"ClusterService"
|
||||
],
|
||||
"summary": "Delete deletes a cluster",
|
||||
"operationId": "Delete",
|
||||
"operationId": "DeleteMixin5",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
|
@ -1243,6 +1243,96 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/gpgkeys": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"GPGKeyService"
|
||||
],
|
||||
"summary": "List all available repository certificates",
|
||||
"operationId": "ListMixin2",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The GPG key ID to query for.",
|
||||
"name": "keyID",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1alpha1GnuPGPublicKeyList"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"GPGKeyService"
|
||||
],
|
||||
"summary": "Create one or more GPG public keys in the server's configuration",
|
||||
"operationId": "CreateMixin2",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1alpha1GnuPGPublicKey"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/gpgkeyGnuPGPublicKeyCreateResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"GPGKeyService"
|
||||
],
|
||||
"summary": "Delete specified GPG public key from the server's configuration",
|
||||
"operationId": "DeleteMixin2",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/gpgkeyGnuPGPublicKeyResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/gpgkeys/{keyID}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"GPGKeyService"
|
||||
],
|
||||
"summary": "Get information about specified GPG public key from the server",
|
||||
"operationId": "GetMixin2",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "keyID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1alpha1GnuPGPublicKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/projects": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
@ -1704,7 +1794,7 @@
|
|||
"RepositoryService"
|
||||
],
|
||||
"summary": "Get returns a repository or its credentials",
|
||||
"operationId": "GetMixin3",
|
||||
"operationId": "Get",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
|
@ -1886,7 +1976,7 @@
|
|||
"SessionService"
|
||||
],
|
||||
"summary": "Create a new JWT for authentication and set a cookie if using HTTP.",
|
||||
"operationId": "CreateMixin10",
|
||||
"operationId": "CreateMixin11",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
|
|
@ -1911,7 +2001,7 @@
|
|||
"SessionService"
|
||||
],
|
||||
"summary": "Delete an existing JWT cookie if using HTTP.",
|
||||
"operationId": "DeleteMixin10",
|
||||
"operationId": "DeleteMixin11",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "(empty)",
|
||||
|
|
@ -1945,7 +2035,7 @@
|
|||
"SettingsService"
|
||||
],
|
||||
"summary": "Get returns Argo CD settings",
|
||||
"operationId": "Get",
|
||||
"operationId": "GetMixin8",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "(empty)",
|
||||
|
|
@ -2440,6 +2530,26 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"gpgkeyGnuPGPublicKeyCreateResponse": {
|
||||
"type": "object",
|
||||
"title": "Response to a public key creation request",
|
||||
"properties": {
|
||||
"created": {
|
||||
"$ref": "#/definitions/v1alpha1GnuPGPublicKeyList"
|
||||
},
|
||||
"skipped": {
|
||||
"type": "array",
|
||||
"title": "List of key IDs that haven been skipped because they already exist on the server",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gpgkeyGnuPGPublicKeyResponse": {
|
||||
"type": "object",
|
||||
"title": "Generic (empty) response for GPG public key CRUD requests"
|
||||
},
|
||||
"oidcClaim": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -2687,6 +2797,10 @@
|
|||
},
|
||||
"sourceType": {
|
||||
"type": "string"
|
||||
},
|
||||
"verifyResult": {
|
||||
"type": "string",
|
||||
"title": "Raw response of git verify-commit operation (always the empty string for Helm)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -3247,6 +3361,13 @@
|
|||
"$ref": "#/definitions/v1alpha1ProjectRole"
|
||||
}
|
||||
},
|
||||
"signatureKeys": {
|
||||
"type": "array",
|
||||
"title": "List of PGP key IDs that commits to be synced to must be signed with",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1SignatureKey"
|
||||
}
|
||||
},
|
||||
"sourceRepos": {
|
||||
"type": "array",
|
||||
"title": "SourceRepos contains list of repository URLs which can be used for deployment",
|
||||
|
|
@ -3775,6 +3896,51 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1GnuPGPublicKey": {
|
||||
"type": "object",
|
||||
"title": "GnuPGPublicKey is a representation of a GnuPG public key",
|
||||
"properties": {
|
||||
"fingerprint": {
|
||||
"type": "string",
|
||||
"title": "Fingerprint of the key"
|
||||
},
|
||||
"keyData": {
|
||||
"type": "string",
|
||||
"title": "Key data"
|
||||
},
|
||||
"keyID": {
|
||||
"type": "string",
|
||||
"title": "KeyID in hexadecimal string format"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"title": "Owner identification"
|
||||
},
|
||||
"subType": {
|
||||
"type": "string",
|
||||
"title": "Key sub type (e.g. rsa4096)"
|
||||
},
|
||||
"trust": {
|
||||
"type": "string",
|
||||
"title": "Trust level"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1GnuPGPublicKeyList": {
|
||||
"type": "object",
|
||||
"title": "GnuPGPublicKeyList is a collection of GnuPGPublicKey objects",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1GnuPGPublicKey"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "#/definitions/v1ListMeta"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1HealthStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -4521,6 +4687,10 @@
|
|||
"type": "string",
|
||||
"title": "the message associated with the revision,\nprobably the commit message,\nthis is truncated to the first newline or 64 characters (which ever comes first)"
|
||||
},
|
||||
"signatureInfo": {
|
||||
"type": "string",
|
||||
"title": "If revision was signed with GPG, and signature verification is enabled,\nthis contains a hint on the signer"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"title": "tags on the revision,\nnote - tags can move from one revision to another",
|
||||
|
|
@ -4530,6 +4700,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SignatureKey": {
|
||||
"type": "object",
|
||||
"title": "SignatureKey is the specification of a key required to verify commit signatures with",
|
||||
"properties": {
|
||||
"keyID": {
|
||||
"type": "string",
|
||||
"title": "The ID of the key in hexadecimal notation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SyncOperation": {
|
||||
"description": "SyncOperation contains sync operation details.",
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -19,14 +19,24 @@ import (
|
|||
"github.com/argoproj/argo-cd/reposerver/metrics"
|
||||
cacheutil "github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-repo-server"
|
||||
cliName = "argocd-repo-server"
|
||||
gnuPGSourcePath = "/app/config/gpg/source"
|
||||
)
|
||||
|
||||
func getGnuPGSourcePath() string {
|
||||
if path := os.Getenv("ARGOCD_GPG_DATA_PATH"); path != "" {
|
||||
return path
|
||||
} else {
|
||||
return gnuPGSourcePath
|
||||
}
|
||||
}
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
logFormat string
|
||||
|
|
@ -63,6 +73,19 @@ func newCommand() *cobra.Command {
|
|||
http.Handle("/metrics", metricsServer.GetHandler())
|
||||
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", metricsPort), nil)) }()
|
||||
|
||||
if gpg.IsGPGEnabled() {
|
||||
log.Infof("Initializing GnuPG keyring at %s", common.GetGnuPGHomePath())
|
||||
err = gpg.InitializeGnuPG()
|
||||
errors.CheckError(err)
|
||||
|
||||
log.Infof("Populating GnuPG keyring with keys from %s", getGnuPGSourcePath())
|
||||
added, removed, err := gpg.SyncKeyRingFromDirectory(getGnuPGSourcePath())
|
||||
errors.CheckError(err)
|
||||
log.Infof("Loaded %d (and removed %d) keys from keyring", len(added), len(removed))
|
||||
|
||||
go func() { errors.CheckError(reposerver.StartGPGWatcher(getGnuPGSourcePath())) }()
|
||||
}
|
||||
|
||||
log.Infof("argocd-repo-server %s serving on %s", common.GetVersion(), listener.Addr())
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
|
|
|
|||
162
cmd/argocd/commands/gpg.go
Normal file
162
cmd/argocd/commands/gpg.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
// NewGPGCommand returns a new instance of an `argocd repo` command
|
||||
func NewGPGCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "gpg",
|
||||
Short: "Manage GPG keys used for signature verification",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
Example: ``,
|
||||
}
|
||||
command.AddCommand(NewGPGListCommand(clientOpts))
|
||||
command.AddCommand(NewGPGGetCommand(clientOpts))
|
||||
command.AddCommand(NewGPGAddCommand(clientOpts))
|
||||
command.AddCommand(NewGPGDeleteCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
// NewGPGListCommand lists all configured public keys from the server
|
||||
func NewGPGListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List configured GPG public keys",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
keys, err := gpgIf.List(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(keys.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "wide", "":
|
||||
printKeyTable(keys.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewGPGGetCommand retrieves a single public key from the server
|
||||
func NewGPGGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "get KEYID",
|
||||
Short: "Get the GPG public key with ID <KEYID> from the server",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
errors.CheckError(fmt.Errorf("Missing KEYID argument"))
|
||||
}
|
||||
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
key, err := gpgIf.Get(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{KeyID: args[0]})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(key, output, false)
|
||||
errors.CheckError(err)
|
||||
case "wide", "":
|
||||
fmt.Printf("Key ID: %s\n", key.KeyID)
|
||||
fmt.Printf("Key fingerprint: %s\n", key.Fingerprint)
|
||||
fmt.Printf("Key subtype: %s\n", strings.ToUpper(key.SubType))
|
||||
fmt.Printf("Key owner: %s\n", key.Owner)
|
||||
fmt.Printf("Key data follows until EOF:\n%s\n", key.KeyData)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewGPGAddCommand adds a public key to the server's configuration
|
||||
func NewGPGAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
fromFile string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Adds a GPG public key to the server's keyring",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if fromFile == "" {
|
||||
errors.CheckError(fmt.Errorf("--from is mandatory"))
|
||||
}
|
||||
keyData, err := ioutil.ReadFile(fromFile)
|
||||
if err != nil {
|
||||
errors.CheckError(err)
|
||||
}
|
||||
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
resp, err := gpgIf.Create(context.Background(), &gpgkeypkg.GnuPGPublicKeyCreateRequest{Publickey: &appsv1.GnuPGPublicKey{KeyData: string(keyData)}})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Created %d key(s) from input file", len(resp.Created.Items))
|
||||
if len(resp.Skipped) > 0 {
|
||||
fmt.Printf(", and %d key(s) were skipped because they exist already", len(resp.Skipped))
|
||||
}
|
||||
fmt.Printf(".\n")
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&fromFile, "from", "f", "", "Path to the file that contains the GPG public key to import")
|
||||
return command
|
||||
|
||||
}
|
||||
|
||||
// NewGPGDeleteCommand removes a key from the server's keyring
|
||||
func NewGPGDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm KEYID",
|
||||
Short: "Removes a GPG public key from the server's keyring",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
errors.CheckError(fmt.Errorf("Missing KEYID argument"))
|
||||
}
|
||||
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
_, err := gpgIf.Delete(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{KeyID: args[0]})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Deleted key with key ID %s\n", args[0])
|
||||
},
|
||||
}
|
||||
return command
|
||||
|
||||
}
|
||||
|
||||
// Print table of certificate info
|
||||
func printKeyTable(keys []appsv1.GnuPGPublicKey) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "KEYID\tTYPE\tIDENTITY\n")
|
||||
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", k.KeyID, strings.ToUpper(k.SubType), k.Owner)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
|
@ -28,12 +28,14 @@ import (
|
|||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/config"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
)
|
||||
|
||||
type projectOpts struct {
|
||||
description string
|
||||
destinations []string
|
||||
sources []string
|
||||
signatureKeys []string
|
||||
orphanedResourcesEnabled bool
|
||||
orphanedResourcesWarn bool
|
||||
}
|
||||
|
|
@ -60,6 +62,18 @@ func (opts *projectOpts) GetDestinations() []v1alpha1.ApplicationDestination {
|
|||
return destinations
|
||||
}
|
||||
|
||||
// TODO: Get configured keys and emit warning when a key is specified that is not configured
|
||||
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)
|
||||
}
|
||||
signatureKeys = append(signatureKeys, v1alpha1.SignatureKey{KeyID: gpg.KeyID(keyStr)})
|
||||
}
|
||||
return signatureKeys
|
||||
}
|
||||
|
||||
// NewProjectCommand returns a new instance of an `argocd proj` command
|
||||
func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
|
|
@ -77,6 +91,8 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
|||
command.AddCommand(NewProjectListCommand(clientOpts))
|
||||
command.AddCommand(NewProjectSetCommand(clientOpts))
|
||||
command.AddCommand(NewProjectEditCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddSignatureKeyCommand(clientOpts))
|
||||
command.AddCommand(NewProjectRemoveSignatureKeyCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddDestinationCommand(clientOpts))
|
||||
command.AddCommand(NewProjectRemoveDestinationCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddSourceCommand(clientOpts))
|
||||
|
|
@ -94,6 +110,7 @@ func addProjFlags(command *cobra.Command, opts *projectOpts) {
|
|||
command.Flags().StringArrayVarP(&opts.destinations, "dest", "d", []string{},
|
||||
"Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)")
|
||||
command.Flags().StringArrayVarP(&opts.sources, "src", "s", []string{}, "Permitted source repository URL")
|
||||
command.Flags().StringSliceVar(&opts.signatureKeys, "signature-keys", []string{}, "GnuPG public key IDs for commit signature verification")
|
||||
command.Flags().BoolVar(&opts.orphanedResourcesEnabled, "orphaned-resources", false, "Enables orphaned resources monitoring")
|
||||
command.Flags().BoolVar(&opts.orphanedResourcesWarn, "orphaned-resources-warn", false, "Specifies if applications should be a warning condition when orphaned resources detected")
|
||||
}
|
||||
|
|
@ -133,6 +150,7 @@ func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
|||
Short: "Create a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
var proj v1alpha1.AppProject
|
||||
fmt.Printf("EE: %d/%v\n", len(opts.signatureKeys), opts.signatureKeys)
|
||||
if fileURL == "-" {
|
||||
// read stdin
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
|
@ -165,6 +183,7 @@ func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
|||
Description: opts.description,
|
||||
Destinations: opts.GetDestinations(),
|
||||
SourceRepos: opts.sources,
|
||||
SignatureKeys: opts.GetSignatureKeys(),
|
||||
OrphanedResources: getOrphanedResourcesSettings(c, opts),
|
||||
},
|
||||
}
|
||||
|
|
@ -215,6 +234,8 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
|||
proj.Spec.Destinations = opts.GetDestinations()
|
||||
case "src":
|
||||
proj.Spec.SourceRepos = opts.sources
|
||||
case "signature-keys":
|
||||
proj.Spec.SignatureKeys = opts.GetSignatureKeys()
|
||||
case "orphaned-resources", "orphaned-resources-warn":
|
||||
proj.Spec.OrphanedResources = getOrphanedResourcesSettings(c, opts)
|
||||
}
|
||||
|
|
@ -233,6 +254,81 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
|||
return command
|
||||
}
|
||||
|
||||
// NewProjectAddSignatureKeyCommand returns a new instance of an `argocd proj add-destination` command
|
||||
func NewProjectAddSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "add-signature-key PROJECT KEY-ID",
|
||||
Short: "Add GnuPG signature key to project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
signatureKey := args[1]
|
||||
|
||||
if !gpg.IsShortKeyID(signatureKey) && !gpg.IsLongKeyID(signatureKey) {
|
||||
log.Fatalf("%s is not a valid GnuPG key ID", signatureKey)
|
||||
}
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, key := range proj.Spec.SignatureKeys {
|
||||
if key.KeyID == signatureKey {
|
||||
log.Fatal("Specified signature key is already defined in project")
|
||||
}
|
||||
}
|
||||
proj.Spec.SignatureKeys = append(proj.Spec.SignatureKeys, v1alpha1.SignatureKey{KeyID: signatureKey})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRemoveDestinationCommand returns a new instance of an `argocd proj remove-destination` command
|
||||
func NewProjectRemoveSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "remove-signature-key PROJECT KEY-ID",
|
||||
Short: "Remove GnuPG signature key from project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
signatureKey := args[1]
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
index := -1
|
||||
for i, key := range proj.Spec.SignatureKeys {
|
||||
if key.KeyID == signatureKey {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
log.Fatal("Specified signature key is not configured for project")
|
||||
} else {
|
||||
proj.Spec.SignatureKeys = append(proj.Spec.SignatureKeys[:index], proj.Spec.SignatureKeys[index+1:]...)
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectAddDestinationCommand returns a new instance of an `argocd proj add-destination` command
|
||||
func NewProjectAddDestinationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
|
|
@ -571,7 +667,7 @@ func printProjectNames(projects []v1alpha1.AppProject) {
|
|||
// Print table of project info
|
||||
func printProjectTable(projects []v1alpha1.AppProject) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\tORPHANED-RESOURCES\n")
|
||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\tSIGNATURE-KEYS\tORPHANED-RESOURCES\n")
|
||||
for _, p := range projects {
|
||||
printProjectLine(w, &p)
|
||||
}
|
||||
|
|
@ -616,7 +712,7 @@ func formatOrphanedResources(p *v1alpha1.AppProject) string {
|
|||
}
|
||||
|
||||
func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
|
||||
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist string
|
||||
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys string
|
||||
switch len(p.Spec.Destinations) {
|
||||
case 0:
|
||||
destinations = "<none>"
|
||||
|
|
@ -647,7 +743,13 @@ func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
|
|||
default:
|
||||
namespaceBlacklist = fmt.Sprintf("%d resources", len(p.Spec.NamespaceResourceBlacklist))
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, formatOrphanedResources(p))
|
||||
switch len(p.Spec.SignatureKeys) {
|
||||
case 0:
|
||||
signatureKeys = "<none>"
|
||||
default:
|
||||
signatureKeys = fmt.Sprintf("%d key(s)", len(p.Spec.SignatureKeys))
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys, formatOrphanedResources(p))
|
||||
}
|
||||
|
||||
func printProject(p *v1alpha1.AppProject) {
|
||||
|
|
@ -695,6 +797,18 @@ func printProject(p *v1alpha1.AppProject) {
|
|||
for i := 1; i < len(p.Spec.NamespaceResourceBlacklist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[i].Group, p.Spec.NamespaceResourceBlacklist[i].Kind))
|
||||
}
|
||||
|
||||
// Print required signature keys
|
||||
signatureKeysStr := "<none>"
|
||||
if len(p.Spec.SignatureKeys) > 0 {
|
||||
kids := make([]string, 0)
|
||||
for _, key := range p.Spec.SignatureKeys {
|
||||
kids = append(kids, key.KeyID)
|
||||
}
|
||||
signatureKeysStr = strings.Join(kids, ", ")
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Signature keys:", signatureKeysStr)
|
||||
|
||||
fmt.Printf(printProjFmtStr, "Orphaned Resources:", formatOrphanedResources(p))
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ func NewCommand() *cobra.Command {
|
|||
command.AddCommand(NewAccountCommand(&clientOpts))
|
||||
command.AddCommand(NewLogoutCommand(&clientOpts))
|
||||
command.AddCommand(NewCertCommand(&clientOpts))
|
||||
command.AddCommand(NewGPGCommand(&clientOpts))
|
||||
|
||||
defaultLocalConfigPath, err := localconfig.DefaultLocalConfigPath()
|
||||
errors.CheckError(err)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const (
|
|||
ArgoCDKnownHostsConfigMapName = "argocd-ssh-known-hosts-cm"
|
||||
// Contains TLS certificate data for connecting repositories. Will get mounted as volume to pods
|
||||
ArgoCDTLSCertsConfigMapName = "argocd-tls-certs-cm"
|
||||
ArgoCDGPGKeysConfigMapName = "argocd-gpg-keys-cm"
|
||||
)
|
||||
|
||||
// Some default configurables
|
||||
|
|
@ -50,6 +51,8 @@ const (
|
|||
DefaultPathSSHConfig = "/app/config/ssh"
|
||||
// Default name for the SSH known hosts file
|
||||
DefaultSSHKnownHostsName = "ssh_known_hosts"
|
||||
// Default path to GnuPG home directory
|
||||
DefaultGnuPgHomePath = "/app/config/gpg/keys"
|
||||
)
|
||||
|
||||
// Argo CD application related constants
|
||||
|
|
@ -148,6 +151,8 @@ const (
|
|||
EnvK8sClientBurst = "ARGOCD_K8S_CLIENT_BURST"
|
||||
// EnvK8sClientMaxIdleConnections is the number of max idle connections in K8s REST client HTTP transport (default: 500)
|
||||
EnvK8sClientMaxIdleConnections = "ARGOCD_K8S_CLIENT_MAX_IDLE_CONNECTIONS"
|
||||
// EnvGnuPGHome is the path to ArgoCD's GnuPG keyring for signature verification
|
||||
EnvGnuPGHome = "ARGOCD_GNUPGHOME"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -160,6 +165,15 @@ const (
|
|||
CacheVersion = "1.0.0"
|
||||
)
|
||||
|
||||
// GetGnuPGHomePath retrieves the path to use for GnuPG home directory, which is either taken from GNUPGHOME environment or a default value
|
||||
func GetGnuPGHomePath() string {
|
||||
if gnuPgHome := os.Getenv(EnvGnuPGHome); gnuPgHome == "" {
|
||||
return DefaultGnuPgHomePath
|
||||
} else {
|
||||
return gnuPgHome
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
|
||||
K8sClientConfigQPS float32 = 50
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import (
|
|||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
argohealth "github.com/argoproj/argo-cd/util/health"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
|
|
@ -93,7 +94,7 @@ type appStateManager struct {
|
|||
namespace string
|
||||
}
|
||||
|
||||
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache bool) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
|
||||
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache, verifySignature bool) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
|
||||
ts := stats.NewTimingStats()
|
||||
helmRepos, err := m.db.ListHelmRepositories(context.Background())
|
||||
if err != nil {
|
||||
|
|
@ -152,6 +153,7 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
|||
KustomizeOptions: kustomizeOptions,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: argo.APIGroupsToVersions(apiGroups),
|
||||
VerifySignature: verifySignature,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
@ -242,6 +244,50 @@ func (m *appStateManager) getComparisonSettings(app *appv1.Application) (string,
|
|||
return appLabelKey, resourceOverrides, diffNormalizer, resFilter, nil
|
||||
}
|
||||
|
||||
// verifyGnuPGSignature verifies the result of a GnuPG operation for a given git
|
||||
// revision.
|
||||
func verifyGnuPGSignature(revision string, project *appv1.AppProject, manifestInfo *apiclient.ManifestResponse) []appv1.ApplicationCondition {
|
||||
now := metav1.Now()
|
||||
conditions := make([]appv1.ApplicationCondition, 0)
|
||||
// We need to have some data in the verificatin result to parse, otherwise there was no signature
|
||||
if manifestInfo.VerifyResult != "" {
|
||||
verifyResult, err := gpg.ParseGitCommitVerification(manifestInfo.VerifyResult)
|
||||
if err != nil {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
log.Errorf("Error while verifying git commit for revision %s: %s", revision, err.Error())
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
// CompareAppState compares application git state to the live app state, using the specified
|
||||
// revision and supplied source. If revision or overrides are empty, then compares against
|
||||
// revision and overrides in the app spec.
|
||||
|
|
@ -261,6 +307,12 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
|||
}
|
||||
}
|
||||
|
||||
// When signature keys are defined in the project spec, we need to verify the signature on the Git revision
|
||||
verifySignature := false
|
||||
if project.Spec.SignatureKeys != nil && len(project.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled() {
|
||||
verifySignature = true
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
@ -273,18 +325,27 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
|||
now := metav1.Now()
|
||||
|
||||
if len(localManifests) == 0 {
|
||||
targetObjs, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache)
|
||||
targetObjs, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache, verifySignature)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
} else {
|
||||
targetObjs, err = unmarshalManifests(localManifests)
|
||||
if err != nil {
|
||||
// Prevent applying local manifests for now when signature verification is enabled
|
||||
// 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"
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
} else {
|
||||
targetObjs, err = unmarshalManifests(localManifests)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
}
|
||||
manifestInfo = nil
|
||||
}
|
||||
|
|
@ -454,6 +515,13 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
|||
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: 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 oppinion about the result
|
||||
// and stop processing if we do not agree about the outcome.
|
||||
if gpg.IsGPGEnabled() && verifySignature && manifestInfo != nil {
|
||||
conditions = append(conditions, verifyGnuPGSignature(revision, project, manifestInfo)...)
|
||||
}
|
||||
|
||||
compRes := comparisonResult{
|
||||
syncStatus: &syncStatus,
|
||||
healthStatus: healthStatus,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package controller
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -440,3 +442,280 @@ func Test_appStateManager_persistRevisionHistory(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, app.Status.History.LastRevisionHistory().DeployStartedAt, &metav1NowTime)
|
||||
}
|
||||
|
||||
// helper function to read contents of a file to string
|
||||
// panics on error
|
||||
func mustReadFile(path string) string {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
var signedProj = argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
SignatureKeys: []argoappv1.SignatureKey{
|
||||
{
|
||||
KeyID: "4AEE18F83AFDEB23",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSignedResponseNoSignatureRequired(t *testing.T) {
|
||||
oldval := os.Getenv("ARGOCD_GPG_ENABLED")
|
||||
os.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
defer os.Setenv("ARGOCD_GPG_ENABLED", oldval)
|
||||
// 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(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
// 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"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignedResponseSignatureRequired(t *testing.T) {
|
||||
oldval := os.Getenv("ARGOCD_GPG_ENABLED")
|
||||
os.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
defer os.Setenv("ARGOCD_GPG_ENABLED", oldval)
|
||||
|
||||
// We have a good signature response, valid key, and signing is required - 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"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
// We have a bad 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_bad.txt"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
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(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
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(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
}
|
||||
|
||||
// We have a good signature and signing is required, but key is not allowed - 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"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
testProj := signedProj
|
||||
testProj.Spec.SignatureKeys[0].KeyID = "4AEE18F83AFDEB24"
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &testProj, "abc123", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
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(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, localManifests)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Contains(t, app.Status.Conditions[0].Message, "Cannot use local manifests")
|
||||
}
|
||||
|
||||
os.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(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
|
||||
// Signature required and local manifests supplied and GPG subystem is disabled - 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{""}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, localManifests)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
305
docs/user-guide/gpg-verification.md
Normal file
305
docs/user-guide/gpg-verification.md
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
# GnuPG signature verifcation
|
||||
|
||||
## 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` and `argocd-application-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"
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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
|
||||
|
||||
Similarily, 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: *
|
||||
Whitelisted Cluster Resources: */*
|
||||
Blacklisted 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` and
|
||||
`argocd-application-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.
|
||||
1
go.mod
1
go.mod
|
|
@ -19,6 +19,7 @@ require (
|
|||
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/evanphx/json-patch v4.5.0+incompatible
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/ghodss/yaml v1.0.0
|
||||
github.com/go-openapi/loads v0.19.2
|
||||
github.com/go-openapi/runtime v0.19.0
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -61,10 +62,33 @@ func newCommand() *cobra.Command {
|
|||
log.Warnf("Failed to create directory: %v", err)
|
||||
return
|
||||
}
|
||||
for name, data := range cm.Data {
|
||||
err := ioutil.WriteFile(path.Join(destPath, name), []byte(data), 0644)
|
||||
// Remove files that do not exist in ConfigMap anymore
|
||||
err = filepath.Walk(destPath, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Warnf("Failed to create file: %v", err)
|
||||
log.Warnf("Error walking path %s: %v", path, err)
|
||||
}
|
||||
p := filepath.Base(path)
|
||||
if _, ok := cm.Data[p]; !ok {
|
||||
log.Infof("Removing file '%s'", path)
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to remove file %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
// Create or update files that are specified in ConfigMap
|
||||
for name, data := range cm.Data {
|
||||
p := path.Join(destPath, name)
|
||||
err := ioutil.WriteFile(p, []byte(data), 0644)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to create file %s: %v", p, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ clean_swagger() {
|
|||
}
|
||||
|
||||
echo "If additional types are added, the number of expected collisions may need to be increased"
|
||||
EXPECTED_COLLISION_COUNT=32
|
||||
EXPECTED_COLLISION_COUNT=33
|
||||
collect_swagger server ${EXPECTED_COLLISION_COUNT}
|
||||
clean_swagger server
|
||||
clean_swagger reposerver
|
||||
|
|
|
|||
46
hack/git-verify-wrapper.sh
Executable file
46
hack/git-verify-wrapper.sh
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/bin/sh
|
||||
# Wrapper script to perform GPG signature validation on git commit SHAs and
|
||||
# annotated tags.
|
||||
#
|
||||
# We capture stderr to stdout, so we can have the output in the logs. Also,
|
||||
# we ignore error codes that are emitted if signature verification failed.
|
||||
#
|
||||
if test "$1" = ""; then
|
||||
echo "Wrong usage of git-verify-wrapper.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REVISION="$1"
|
||||
TYPE=
|
||||
|
||||
# Figure out we have an annotated tag or a commit SHA
|
||||
if git describe --exact-match "${REVISION}" >/dev/null 2>&1; then
|
||||
IFS=''
|
||||
TYPE=tag
|
||||
OUTPUT=$(git verify-tag "$REVISION" 2>&1)
|
||||
RET=$?
|
||||
else
|
||||
IFS=''
|
||||
TYPE=commit
|
||||
OUTPUT=$(git verify-commit "$REVISION" 2>&1)
|
||||
RET=$?
|
||||
fi
|
||||
|
||||
case "$RET" in
|
||||
0)
|
||||
echo "$OUTPUT"
|
||||
;;
|
||||
1)
|
||||
# git verify-tag emits error messages if no signature is found on tag,
|
||||
# which we don't want in the output.
|
||||
if test "$TYPE" = "tag" -a "${OUTPUT%%:*}" = "error"; then
|
||||
OUTPUT=""
|
||||
fi
|
||||
echo "$OUTPUT"
|
||||
RET=0
|
||||
;;
|
||||
*)
|
||||
echo "$OUTPUT" >&2
|
||||
;;
|
||||
esac
|
||||
exit $RET
|
||||
19
hack/gpg-wrapper.sh
Executable file
19
hack/gpg-wrapper.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/sh
|
||||
# Simple wrapper around gpg to prevent exit code != 0
|
||||
ARGS=$*
|
||||
OUTPUT=$(gpg $ARGS 2>&1)
|
||||
IFS=''
|
||||
RET=$?
|
||||
case "$RET" in
|
||||
0)
|
||||
echo $OUTPUT
|
||||
;;
|
||||
1)
|
||||
echo $OUTPUT
|
||||
RET=0
|
||||
;;
|
||||
*)
|
||||
echo $OUTPUT >&2
|
||||
;;
|
||||
esac
|
||||
exit $RET
|
||||
7
manifests/base/config/argocd-gpg-keys-cm.yaml
Normal file
7
manifests/base/config/argocd-gpg-keys-cm.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-gpg-keys-cm
|
||||
app.kubernetes.io/part-of: argocd
|
||||
name: argocd-gpg-keys-cm
|
||||
|
|
@ -7,3 +7,4 @@ resources:
|
|||
- argocd-rbac-cm.yaml
|
||||
- argocd-ssh-known-hosts-cm.yaml
|
||||
- argocd-tls-certs-cm.yaml
|
||||
- argocd-gpg-keys-cm.yaml
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ spec:
|
|||
mountPath: /app/config/ssh
|
||||
- name: tls-certs
|
||||
mountPath: /app/config/tls
|
||||
- name: gpg-keys
|
||||
mountPath: /app/config/gpg/source
|
||||
volumes:
|
||||
- name: ssh-known-hosts
|
||||
configMap:
|
||||
|
|
@ -50,3 +52,6 @@ spec:
|
|||
- name: tls-certs
|
||||
configMap:
|
||||
name: argocd-tls-certs-cm
|
||||
- name: gpg-keys
|
||||
configMap:
|
||||
name: argocd-gpg-keys-cm
|
||||
|
|
|
|||
|
|
@ -170,6 +170,20 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
type: array
|
||||
signatureKeys:
|
||||
description: List of PGP key IDs that commits to be synced to must be
|
||||
signed with
|
||||
items:
|
||||
description: SignatureKey is the specification of a key required to
|
||||
verify commit signatures with
|
||||
properties:
|
||||
keyID:
|
||||
description: The ID of the key in hexadecimal notation
|
||||
type: string
|
||||
required:
|
||||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceRepos:
|
||||
description: SourceRepos contains list of repository URLs which can
|
||||
be used for deployment
|
||||
|
|
|
|||
|
|
@ -1904,6 +1904,20 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
type: array
|
||||
signatureKeys:
|
||||
description: List of PGP key IDs that commits to be synced to must be
|
||||
signed with
|
||||
items:
|
||||
description: SignatureKey is the specification of a key required to
|
||||
verify commit signatures with
|
||||
properties:
|
||||
keyID:
|
||||
description: The ID of the key in hexadecimal notation
|
||||
type: string
|
||||
required:
|
||||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceRepos:
|
||||
description: SourceRepos contains list of repository URLs which can
|
||||
be used for deployment
|
||||
|
|
@ -2539,6 +2553,14 @@ metadata:
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-gpg-keys-cm
|
||||
app.kubernetes.io/part-of: argocd
|
||||
name: argocd-gpg-keys-cm
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-rbac-cm
|
||||
|
|
@ -3045,6 +3067,8 @@ spec:
|
|||
name: ssh-known-hosts
|
||||
- mountPath: /app/config/tls
|
||||
name: tls-certs
|
||||
- mountPath: /app/config/gpg/source
|
||||
name: gpg-keys
|
||||
volumes:
|
||||
- configMap:
|
||||
name: argocd-ssh-known-hosts-cm
|
||||
|
|
@ -3052,6 +3076,9 @@ spec:
|
|||
- configMap:
|
||||
name: argocd-tls-certs-cm
|
||||
name: tls-certs
|
||||
- configMap:
|
||||
name: argocd-gpg-keys-cm
|
||||
name: gpg-keys
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
|
|
|
|||
|
|
@ -1904,6 +1904,20 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
type: array
|
||||
signatureKeys:
|
||||
description: List of PGP key IDs that commits to be synced to must be
|
||||
signed with
|
||||
items:
|
||||
description: SignatureKey is the specification of a key required to
|
||||
verify commit signatures with
|
||||
properties:
|
||||
keyID:
|
||||
description: The ID of the key in hexadecimal notation
|
||||
type: string
|
||||
required:
|
||||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceRepos:
|
||||
description: SourceRepos contains list of repository URLs which can
|
||||
be used for deployment
|
||||
|
|
@ -2454,6 +2468,14 @@ metadata:
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-gpg-keys-cm
|
||||
app.kubernetes.io/part-of: argocd
|
||||
name: argocd-gpg-keys-cm
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-rbac-cm
|
||||
|
|
@ -2960,6 +2982,8 @@ spec:
|
|||
name: ssh-known-hosts
|
||||
- mountPath: /app/config/tls
|
||||
name: tls-certs
|
||||
- mountPath: /app/config/gpg/source
|
||||
name: gpg-keys
|
||||
volumes:
|
||||
- configMap:
|
||||
name: argocd-ssh-known-hosts-cm
|
||||
|
|
@ -2967,6 +2991,9 @@ spec:
|
|||
- configMap:
|
||||
name: argocd-tls-certs-cm
|
||||
name: tls-certs
|
||||
- configMap:
|
||||
name: argocd-gpg-keys-cm
|
||||
name: gpg-keys
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
|
|
|
|||
|
|
@ -1904,6 +1904,20 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
type: array
|
||||
signatureKeys:
|
||||
description: List of PGP key IDs that commits to be synced to must be
|
||||
signed with
|
||||
items:
|
||||
description: SignatureKey is the specification of a key required to
|
||||
verify commit signatures with
|
||||
properties:
|
||||
keyID:
|
||||
description: The ID of the key in hexadecimal notation
|
||||
type: string
|
||||
required:
|
||||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceRepos:
|
||||
description: SourceRepos contains list of repository URLs which can
|
||||
be used for deployment
|
||||
|
|
@ -2234,6 +2248,14 @@ metadata:
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-gpg-keys-cm
|
||||
app.kubernetes.io/part-of: argocd
|
||||
name: argocd-gpg-keys-cm
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-rbac-cm
|
||||
|
|
@ -2559,6 +2581,8 @@ spec:
|
|||
name: ssh-known-hosts
|
||||
- mountPath: /app/config/tls
|
||||
name: tls-certs
|
||||
- mountPath: /app/config/gpg/source
|
||||
name: gpg-keys
|
||||
volumes:
|
||||
- configMap:
|
||||
name: argocd-ssh-known-hosts-cm
|
||||
|
|
@ -2566,6 +2590,9 @@ spec:
|
|||
- configMap:
|
||||
name: argocd-tls-certs-cm
|
||||
name: tls-certs
|
||||
- configMap:
|
||||
name: argocd-gpg-keys-cm
|
||||
name: gpg-keys
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
|
|
|
|||
|
|
@ -1904,6 +1904,20 @@ spec:
|
|||
- name
|
||||
type: object
|
||||
type: array
|
||||
signatureKeys:
|
||||
description: List of PGP key IDs that commits to be synced to must be
|
||||
signed with
|
||||
items:
|
||||
description: SignatureKey is the specification of a key required to
|
||||
verify commit signatures with
|
||||
properties:
|
||||
keyID:
|
||||
description: The ID of the key in hexadecimal notation
|
||||
type: string
|
||||
required:
|
||||
- keyID
|
||||
type: object
|
||||
type: array
|
||||
sourceRepos:
|
||||
description: SourceRepos contains list of repository URLs which can
|
||||
be used for deployment
|
||||
|
|
@ -2149,6 +2163,14 @@ metadata:
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-gpg-keys-cm
|
||||
app.kubernetes.io/part-of: argocd
|
||||
name: argocd-gpg-keys-cm
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-rbac-cm
|
||||
|
|
@ -2474,6 +2496,8 @@ spec:
|
|||
name: ssh-known-hosts
|
||||
- mountPath: /app/config/tls
|
||||
name: tls-certs
|
||||
- mountPath: /app/config/gpg/source
|
||||
name: gpg-keys
|
||||
volumes:
|
||||
- configMap:
|
||||
name: argocd-ssh-known-hosts-cm
|
||||
|
|
@ -2481,6 +2505,9 @@ spec:
|
|||
- configMap:
|
||||
name: argocd-tls-certs-cm
|
||||
name: tls-certs
|
||||
- configMap:
|
||||
name: argocd-gpg-keys-cm
|
||||
name: gpg-keys
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ nav:
|
|||
- user-guide/tool_detection.md
|
||||
- user-guide/projects.md
|
||||
- user-guide/private-repositories.md
|
||||
- GnuPG verification: user-guide/gpg-verification.md
|
||||
- user-guide/auto_sync.md
|
||||
- user-guide/diffing.md
|
||||
- user-guide/orphaned-resources.md
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import (
|
|||
applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
|
||||
certificatepkg "github.com/argoproj/argo-cd/pkg/apiclient/certificate"
|
||||
clusterpkg "github.com/argoproj/argo-cd/pkg/apiclient/cluster"
|
||||
gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
repocredspkg "github.com/argoproj/argo-cd/pkg/apiclient/repocreds"
|
||||
repositorypkg "github.com/argoproj/argo-cd/pkg/apiclient/repository"
|
||||
|
|
@ -68,6 +69,8 @@ type Client interface {
|
|||
NewCertClientOrDie() (io.Closer, certificatepkg.CertificateServiceClient)
|
||||
NewClusterClient() (io.Closer, clusterpkg.ClusterServiceClient, error)
|
||||
NewClusterClientOrDie() (io.Closer, clusterpkg.ClusterServiceClient)
|
||||
NewGPGKeyClient() (io.Closer, gpgkeypkg.GPGKeyServiceClient, error)
|
||||
NewGPGKeyClientOrDie() (io.Closer, gpgkeypkg.GPGKeyServiceClient)
|
||||
NewApplicationClient() (io.Closer, applicationpkg.ApplicationServiceClient, error)
|
||||
NewApplicationClientOrDie() (io.Closer, applicationpkg.ApplicationServiceClient)
|
||||
NewSessionClient() (io.Closer, sessionpkg.SessionServiceClient, error)
|
||||
|
|
@ -559,6 +562,23 @@ func (c *client) NewClusterClientOrDie() (io.Closer, clusterpkg.ClusterServiceCl
|
|||
return conn, clusterIf
|
||||
}
|
||||
|
||||
func (c *client) NewGPGKeyClient() (io.Closer, gpgkeypkg.GPGKeyServiceClient, error) {
|
||||
conn, closer, err := c.newConn()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
gpgkeyIf := gpgkeypkg.NewGPGKeyServiceClient(conn)
|
||||
return closer, gpgkeyIf, nil
|
||||
}
|
||||
|
||||
func (c *client) NewGPGKeyClientOrDie() (io.Closer, gpgkeypkg.GPGKeyServiceClient) {
|
||||
conn, gpgkeyIf, err := c.NewGPGKeyClient()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to establish connection to %s: %v", c.ServerAddr, err)
|
||||
}
|
||||
return conn, gpgkeyIf
|
||||
}
|
||||
|
||||
func (c *client) NewApplicationClient() (io.Closer, applicationpkg.ApplicationServiceClient, error) {
|
||||
conn, closer, err := c.newConn()
|
||||
if err != nil {
|
||||
|
|
|
|||
1180
pkg/apiclient/gpgkey/gpgkey.pb.go
Normal file
1180
pkg/apiclient/gpgkey/gpgkey.pb.go
Normal file
File diff suppressed because it is too large
Load diff
288
pkg/apiclient/gpgkey/gpgkey.pb.gw.go
Normal file
288
pkg/apiclient/gpgkey/gpgkey.pb.gw.go
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
|
||||
// source: server/gpgkey/gpgkey.proto
|
||||
|
||||
/*
|
||||
Package gpgkey is a reverse proxy.
|
||||
|
||||
It translates gRPC into RESTful JSON APIs.
|
||||
*/
|
||||
package gpgkey
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/runtime"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/utilities"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ codes.Code
|
||||
var _ io.Reader
|
||||
var _ status.Status
|
||||
var _ = runtime.String
|
||||
var _ = utilities.NewDoubleArray
|
||||
|
||||
var (
|
||||
filter_GPGKeyService_List_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
)
|
||||
|
||||
func request_GPGKeyService_List_0(ctx context.Context, marshaler runtime.Marshaler, client GPGKeyServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GnuPGPublicKeyQuery
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GPGKeyService_List_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.List(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_GPGKeyService_Get_0(ctx context.Context, marshaler runtime.Marshaler, client GPGKeyServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GnuPGPublicKeyQuery
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["keyID"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "keyID")
|
||||
}
|
||||
|
||||
protoReq.KeyID, err = runtime.String(val)
|
||||
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "keyID", err)
|
||||
}
|
||||
|
||||
msg, err := client.Get(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
filter_GPGKeyService_Create_0 = &utilities.DoubleArray{Encoding: map[string]int{"publickey": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
|
||||
)
|
||||
|
||||
func request_GPGKeyService_Create_0(ctx context.Context, marshaler runtime.Marshaler, client GPGKeyServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GnuPGPublicKeyCreateRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Publickey); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GPGKeyService_Create_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.Create(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
filter_GPGKeyService_Delete_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
)
|
||||
|
||||
func request_GPGKeyService_Delete_0(ctx context.Context, marshaler runtime.Marshaler, client GPGKeyServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GnuPGPublicKeyQuery
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GPGKeyService_Delete_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.Delete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
// RegisterGPGKeyServiceHandlerFromEndpoint is same as RegisterGPGKeyServiceHandler but
|
||||
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
|
||||
func RegisterGPGKeyServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
|
||||
conn, err := grpc.Dial(endpoint, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
return RegisterGPGKeyServiceHandler(ctx, mux, conn)
|
||||
}
|
||||
|
||||
// RegisterGPGKeyServiceHandler registers the http handlers for service GPGKeyService to "mux".
|
||||
// The handlers forward requests to the grpc endpoint over "conn".
|
||||
func RegisterGPGKeyServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
|
||||
return RegisterGPGKeyServiceHandlerClient(ctx, mux, NewGPGKeyServiceClient(conn))
|
||||
}
|
||||
|
||||
// RegisterGPGKeyServiceHandler registers the http handlers for service GPGKeyService to "mux".
|
||||
// The handlers forward requests to the grpc endpoint over the given implementation of "GPGKeyServiceClient".
|
||||
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "GPGKeyServiceClient"
|
||||
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
|
||||
// "GPGKeyServiceClient" to call the correct interceptors.
|
||||
func RegisterGPGKeyServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client GPGKeyServiceClient) error {
|
||||
|
||||
mux.Handle("GET", pattern_GPGKeyService_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
go func(done <-chan struct{}, closed <-chan bool) {
|
||||
select {
|
||||
case <-done:
|
||||
case <-closed:
|
||||
cancel()
|
||||
}
|
||||
}(ctx.Done(), cn.CloseNotify())
|
||||
}
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_GPGKeyService_List_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_GPGKeyService_List_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_GPGKeyService_Get_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
go func(done <-chan struct{}, closed <-chan bool) {
|
||||
select {
|
||||
case <-done:
|
||||
case <-closed:
|
||||
cancel()
|
||||
}
|
||||
}(ctx.Done(), cn.CloseNotify())
|
||||
}
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_GPGKeyService_Get_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_GPGKeyService_Get_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_GPGKeyService_Create_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
go func(done <-chan struct{}, closed <-chan bool) {
|
||||
select {
|
||||
case <-done:
|
||||
case <-closed:
|
||||
cancel()
|
||||
}
|
||||
}(ctx.Done(), cn.CloseNotify())
|
||||
}
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_GPGKeyService_Create_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_GPGKeyService_Create_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("DELETE", pattern_GPGKeyService_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
if cn, ok := w.(http.CloseNotifier); ok {
|
||||
go func(done <-chan struct{}, closed <-chan bool) {
|
||||
select {
|
||||
case <-done:
|
||||
case <-closed:
|
||||
cancel()
|
||||
}
|
||||
}(ctx.Done(), cn.CloseNotify())
|
||||
}
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_GPGKeyService_Delete_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_GPGKeyService_Delete_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
pattern_GPGKeyService_List_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "gpgkeys"}, ""))
|
||||
|
||||
pattern_GPGKeyService_Get_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "gpgkeys", "keyID"}, ""))
|
||||
|
||||
pattern_GPGKeyService_Create_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "gpgkeys"}, ""))
|
||||
|
||||
pattern_GPGKeyService_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "gpgkeys"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
forward_GPGKeyService_List_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_GPGKeyService_Get_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_GPGKeyService_Create_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_GPGKeyService_Delete_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
|
|
@ -4,6 +4,7 @@ API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/appli
|
|||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,AppProjectSpec,NamespaceResourceBlacklist
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,AppProjectSpec,NamespaceResourceWhitelist
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,AppProjectSpec,Roles
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,AppProjectSpec,SignatureKeys
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,AppProjectSpec,SourceRepos
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,ApplicationList,Items
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,ApplicationSourceHelm,FileParameters
|
||||
|
|
@ -24,6 +25,7 @@ API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/appli
|
|||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,ClusterList,Items
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,Command,Args
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,Command,Command
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,GnuPGPublicKeyList,Items
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,Operation,Info
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,ProjectRole,Groups
|
||||
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,ProjectRole,JWTTokens
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -74,6 +74,9 @@ message AppProjectSpec {
|
|||
|
||||
// NamespaceResourceWhitelist contains list of whitelisted namespace level resources
|
||||
repeated k8s.io.apimachinery.pkg.apis.meta.v1.GroupKind namespaceResourceWhitelist = 9;
|
||||
|
||||
// List of PGP key IDs that commits to be synced to must be signed with
|
||||
repeated SignatureKey signatureKeys = 10;
|
||||
}
|
||||
|
||||
// Application is a definition of Application resource.
|
||||
|
|
@ -400,6 +403,34 @@ message EnvEntry {
|
|||
optional string value = 2;
|
||||
}
|
||||
|
||||
// GnuPGPublicKey is a representation of a GnuPG public key
|
||||
message GnuPGPublicKey {
|
||||
// KeyID in hexadecimal string format
|
||||
optional string keyID = 1;
|
||||
|
||||
// Fingerprint of the key
|
||||
optional string fingerprint = 2;
|
||||
|
||||
// Owner identification
|
||||
optional string owner = 3;
|
||||
|
||||
// Trust level
|
||||
optional string trust = 4;
|
||||
|
||||
// Key sub type (e.g. rsa4096)
|
||||
optional string subType = 5;
|
||||
|
||||
// Key data
|
||||
optional string keyData = 6;
|
||||
}
|
||||
|
||||
// GnuPGPublicKeyList is a collection of GnuPGPublicKey objects
|
||||
message GnuPGPublicKeyList {
|
||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;
|
||||
|
||||
repeated GnuPGPublicKey items = 2;
|
||||
}
|
||||
|
||||
message HealthStatus {
|
||||
optional string status = 1;
|
||||
|
||||
|
|
@ -867,6 +898,16 @@ message RevisionMetadata {
|
|||
// probably the commit message,
|
||||
// this is truncated to the first newline or 64 characters (which ever comes first)
|
||||
optional string message = 4;
|
||||
|
||||
// If revision was signed with GPG, and signature verification is enabled,
|
||||
// this contains a hint on the signer
|
||||
optional string signatureInfo = 5;
|
||||
}
|
||||
|
||||
// SignatureKey is the specification of a key required to verify commit signatures with
|
||||
message SignatureKey {
|
||||
// The ID of the key in hexadecimal notation
|
||||
optional string keyID = 1;
|
||||
}
|
||||
|
||||
// SyncOperation contains sync operation details.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
|||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ConfigManagementPlugin": schema_pkg_apis_application_v1alpha1_ConfigManagementPlugin(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ConnectionState": schema_pkg_apis_application_v1alpha1_ConnectionState(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.EnvEntry": schema_pkg_apis_application_v1alpha1_EnvEntry(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.GnuPGPublicKey": schema_pkg_apis_application_v1alpha1_GnuPGPublicKey(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.GnuPGPublicKeyList": schema_pkg_apis_application_v1alpha1_GnuPGPublicKeyList(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.HealthStatus": schema_pkg_apis_application_v1alpha1_HealthStatus(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.HelmFileParameter": schema_pkg_apis_application_v1alpha1_HelmFileParameter(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.HelmParameter": schema_pkg_apis_application_v1alpha1_HelmParameter(ref),
|
||||
|
|
@ -77,6 +79,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
|||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ResourceStatus": schema_pkg_apis_application_v1alpha1_ResourceStatus(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.RevisionHistory": schema_pkg_apis_application_v1alpha1_RevisionHistory(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.RevisionMetadata": schema_pkg_apis_application_v1alpha1_RevisionMetadata(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SignatureKey": schema_pkg_apis_application_v1alpha1_SignatureKey(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SyncOperation": schema_pkg_apis_application_v1alpha1_SyncOperation(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SyncOperationResource": schema_pkg_apis_application_v1alpha1_SyncOperationResource(ref),
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SyncOperationResult": schema_pkg_apis_application_v1alpha1_SyncOperationResult(ref),
|
||||
|
|
@ -318,11 +321,24 @@ func schema_pkg_apis_application_v1alpha1_AppProjectSpec(ref common.ReferenceCal
|
|||
},
|
||||
},
|
||||
},
|
||||
"signatureKeys": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "List of PGP key IDs that commits to be synced to must be signed with",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Ref: ref("github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SignatureKey"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ApplicationDestination", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.OrphanedResourcesMonitorSettings", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ProjectRole", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SyncWindow", "k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind"},
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ApplicationDestination", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.OrphanedResourcesMonitorSettings", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ProjectRole", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SignatureKey", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SyncWindow", "k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind"},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1438,6 +1454,95 @@ func schema_pkg_apis_application_v1alpha1_EnvEntry(ref common.ReferenceCallback)
|
|||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_application_v1alpha1_GnuPGPublicKey(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "GnuPGPublicKey is a representation of a GnuPG public key",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"keyID": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "KeyID in hexadecimal string format",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"fingerprint": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Fingerprint of the key",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"owner": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Owner identification",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"trust": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Trust level",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"subType": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Key sub type (e.g. rsa4096)",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"keyData": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Key data",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"keyID"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_application_v1alpha1_GnuPGPublicKeyList(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "GnuPGPublicKeyList is a collection of GnuPGPublicKey objects",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Ref: ref("github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.GnuPGPublicKey"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"items"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.GnuPGPublicKey", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_application_v1alpha1_HealthStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
|
@ -3018,6 +3123,13 @@ func schema_pkg_apis_application_v1alpha1_RevisionMetadata(ref common.ReferenceC
|
|||
Format: "",
|
||||
},
|
||||
},
|
||||
"signatureInfo": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "If revision was signed with GPG, and signature verification is enabled, this contains a hint on the signer",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"date"},
|
||||
},
|
||||
|
|
@ -3027,6 +3139,27 @@ func schema_pkg_apis_application_v1alpha1_RevisionMetadata(ref common.ReferenceC
|
|||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_application_v1alpha1_SignatureKey(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "SignatureKey is the specification of a key required to verify commit signatures with",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"keyID": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The ID of the key in hexadecimal notation",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"keyID"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_application_v1alpha1_SyncOperation(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
|
|
|||
|
|
@ -618,6 +618,9 @@ type RevisionMetadata struct {
|
|||
// probably the commit message,
|
||||
// this is truncated to the first newline or 64 characters (which ever comes first)
|
||||
Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"`
|
||||
// If revision was signed with GPG, and signature verification is enabled,
|
||||
// this contains a hint on the signer
|
||||
SignatureInfo string `json:"signatureInfo,omitempty" protobuf:"bytes,5,opt,name=signatureInfo"`
|
||||
}
|
||||
|
||||
// SyncOperationResult represent result of sync operation
|
||||
|
|
@ -1247,6 +1250,28 @@ type RepositoryCertificateList struct {
|
|||
Items []RepositoryCertificate `json:"items" protobuf:"bytes,2,rep,name=items"`
|
||||
}
|
||||
|
||||
// GnuPGPublicKey is a representation of a GnuPG public key
|
||||
type GnuPGPublicKey struct {
|
||||
// KeyID in hexadecimal string format
|
||||
KeyID string `json:"keyID" protobuf:"bytes,1,opt,name=keyID"`
|
||||
// Fingerprint of the key
|
||||
Fingerprint string `json:"fingerprint,omitempty" protobuf:"bytes,2,opt,name=fingerprint"`
|
||||
// Owner identification
|
||||
Owner string `json:"owner,omitempty" protobuf:"bytes,3,opt,name=owner"`
|
||||
// Trust level
|
||||
Trust string `json:"trust,omitempty" protobuf:"bytes,4,opt,name=trust"`
|
||||
// Key sub type (e.g. rsa4096)
|
||||
SubType string `json:"subType,omitempty" protobuf:"bytes,5,opt,name=subType"`
|
||||
// Key data
|
||||
KeyData string `json:"keyData,omitempty" protobuf:"bytes,6,opt,name=keyData"`
|
||||
}
|
||||
|
||||
// GnuPGPublicKeyList is a collection of GnuPGPublicKey objects
|
||||
type GnuPGPublicKeyList struct {
|
||||
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||
Items []GnuPGPublicKey `json:"items" protobuf:"bytes,2,rep,name=items"`
|
||||
}
|
||||
|
||||
// AppProjectList is list of AppProject resources
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type AppProjectList struct {
|
||||
|
|
@ -1540,6 +1565,12 @@ func (s *OrphanedResourcesMonitorSettings) IsWarn() bool {
|
|||
return s.Warn == nil || *s.Warn
|
||||
}
|
||||
|
||||
// SignatureKey is the specification of a key required to verify commit signatures with
|
||||
type SignatureKey struct {
|
||||
// The ID of the key in hexadecimal notation
|
||||
KeyID string `json:"keyID" protobuf:"bytes,1,name=keyID"`
|
||||
}
|
||||
|
||||
// AppProjectSpec is the specification of an AppProject
|
||||
type AppProjectSpec struct {
|
||||
// SourceRepos contains list of repository URLs which can be used for deployment
|
||||
|
|
@ -1560,6 +1591,8 @@ type AppProjectSpec struct {
|
|||
SyncWindows SyncWindows `json:"syncWindows,omitempty" protobuf:"bytes,8,opt,name=syncWindows"`
|
||||
// NamespaceResourceWhitelist contains list of whitelisted namespace level resources
|
||||
NamespaceResourceWhitelist []metav1.GroupKind `json:"namespaceResourceWhitelist,omitempty" protobuf:"bytes,9,opt,name=namespaceResourceWhitelist"`
|
||||
// List of PGP key IDs that commits to be synced to must be signed with
|
||||
SignatureKeys []SignatureKey `json:"signatureKeys,omitempty" protobuf:"bytes,10,opt,name=signatureKeys"`
|
||||
}
|
||||
|
||||
// SyncWindows is a collection of sync windows in this project
|
||||
|
|
|
|||
|
|
@ -137,6 +137,11 @@ func (in *AppProjectSpec) DeepCopyInto(out *AppProjectSpec) {
|
|||
*out = make([]v1.GroupKind, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.SignatureKeys != nil {
|
||||
in, out := &in.SignatureKeys, &out.SignatureKeys
|
||||
*out = make([]SignatureKey, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -822,6 +827,44 @@ func (in *EnvEntry) DeepCopy() *EnvEntry {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GnuPGPublicKey) DeepCopyInto(out *GnuPGPublicKey) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GnuPGPublicKey.
|
||||
func (in *GnuPGPublicKey) DeepCopy() *GnuPGPublicKey {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GnuPGPublicKey)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GnuPGPublicKeyList) DeepCopyInto(out *GnuPGPublicKeyList) {
|
||||
*out = *in
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]GnuPGPublicKey, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GnuPGPublicKeyList.
|
||||
func (in *GnuPGPublicKeyList) DeepCopy() *GnuPGPublicKeyList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GnuPGPublicKeyList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HealthStatus) DeepCopyInto(out *HealthStatus) {
|
||||
*out = *in
|
||||
|
|
@ -1648,6 +1691,22 @@ func (in *RevisionMetadata) DeepCopy() *RevisionMetadata {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SignatureKey) DeepCopyInto(out *SignatureKey) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SignatureKey.
|
||||
func (in *SignatureKey) DeepCopy() *SignatureKey {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SignatureKey)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SyncOperation) DeepCopyInto(out *SyncOperation) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -34,20 +34,22 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
|
|||
type ManifestRequest struct {
|
||||
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
|
||||
// revision, potentially un-resolved
|
||||
Revision string `protobuf:"bytes,2,opt,name=revision,proto3" json:"revision,omitempty"`
|
||||
NoCache bool `protobuf:"varint,3,opt,name=noCache,proto3" json:"noCache,omitempty"`
|
||||
AppLabelKey string `protobuf:"bytes,4,opt,name=appLabelKey,proto3" json:"appLabelKey,omitempty"`
|
||||
AppLabelValue string `protobuf:"bytes,5,opt,name=appLabelValue,proto3" json:"appLabelValue,omitempty"`
|
||||
Namespace string `protobuf:"bytes,8,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
ApplicationSource *v1alpha1.ApplicationSource `protobuf:"bytes,10,opt,name=applicationSource,proto3" json:"applicationSource,omitempty"`
|
||||
Repos []*v1alpha1.Repository `protobuf:"bytes,11,rep,name=repos,proto3" json:"repos,omitempty"`
|
||||
Plugins []*v1alpha1.ConfigManagementPlugin `protobuf:"bytes,12,rep,name=plugins,proto3" json:"plugins,omitempty"`
|
||||
KustomizeOptions *v1alpha1.KustomizeOptions `protobuf:"bytes,13,opt,name=kustomizeOptions,proto3" json:"kustomizeOptions,omitempty"`
|
||||
KubeVersion string `protobuf:"bytes,14,opt,name=kubeVersion,proto3" json:"kubeVersion,omitempty"`
|
||||
ApiVersions []string `protobuf:"bytes,15,rep,name=apiVersions,proto3" json:"apiVersions,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"`
|
||||
NoCache bool `protobuf:"varint,3,opt,name=noCache,proto3" json:"noCache,omitempty"`
|
||||
AppLabelKey string `protobuf:"bytes,4,opt,name=appLabelKey,proto3" json:"appLabelKey,omitempty"`
|
||||
AppLabelValue string `protobuf:"bytes,5,opt,name=appLabelValue,proto3" json:"appLabelValue,omitempty"`
|
||||
Namespace string `protobuf:"bytes,8,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
ApplicationSource *v1alpha1.ApplicationSource `protobuf:"bytes,10,opt,name=applicationSource,proto3" json:"applicationSource,omitempty"`
|
||||
Repos []*v1alpha1.Repository `protobuf:"bytes,11,rep,name=repos,proto3" json:"repos,omitempty"`
|
||||
Plugins []*v1alpha1.ConfigManagementPlugin `protobuf:"bytes,12,rep,name=plugins,proto3" json:"plugins,omitempty"`
|
||||
KustomizeOptions *v1alpha1.KustomizeOptions `protobuf:"bytes,13,opt,name=kustomizeOptions,proto3" json:"kustomizeOptions,omitempty"`
|
||||
KubeVersion string `protobuf:"bytes,14,opt,name=kubeVersion,proto3" json:"kubeVersion,omitempty"`
|
||||
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"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ManifestRequest) Reset() { *m = ManifestRequest{} }
|
||||
|
|
@ -167,13 +169,22 @@ func (m *ManifestRequest) GetApiVersions() []string {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *ManifestRequest) GetVerifySignature() bool {
|
||||
if m != nil {
|
||||
return m.VerifySignature
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ManifestResponse struct {
|
||||
Manifests []string `protobuf:"bytes,1,rep,name=manifests,proto3" json:"manifests,omitempty"`
|
||||
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
Server string `protobuf:"bytes,3,opt,name=server,proto3" json:"server,omitempty"`
|
||||
// resolved revision
|
||||
Revision string `protobuf:"bytes,4,opt,name=revision,proto3" json:"revision,omitempty"`
|
||||
SourceType string `protobuf:"bytes,6,opt,name=sourceType,proto3" json:"sourceType,omitempty"`
|
||||
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)
|
||||
VerifyResult string `protobuf:"bytes,7,opt,name=verifyResult,proto3" json:"verifyResult,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
|
|
@ -247,6 +258,13 @@ func (m *ManifestResponse) GetSourceType() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (m *ManifestResponse) GetVerifyResult() string {
|
||||
if m != nil {
|
||||
return m.VerifyResult
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ListAppsRequest requests a repository directory structure
|
||||
type ListAppsRequest struct {
|
||||
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
|
||||
|
|
@ -1095,82 +1113,84 @@ func init() {
|
|||
}
|
||||
|
||||
var fileDescriptor_dd8723cfcc820480 = []byte{
|
||||
// 1194 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x57, 0x5b, 0x6f, 0x1b, 0xc5,
|
||||
0x17, 0xcf, 0xda, 0x8e, 0x13, 0x1f, 0xf7, 0xe2, 0x4c, 0xfb, 0xef, 0x7f, 0x31, 0xa9, 0x65, 0x56,
|
||||
0x80, 0x02, 0xa5, 0x6b, 0x12, 0x2a, 0x11, 0x15, 0xa9, 0x92, 0x49, 0xd2, 0x14, 0x39, 0x51, 0xd3,
|
||||
0x0d, 0x54, 0xe2, 0x22, 0x55, 0x93, 0xf5, 0x64, 0x3d, 0x78, 0xbd, 0x3b, 0xec, 0x8c, 0x8d, 0xd2,
|
||||
0x2f, 0x00, 0x12, 0x8f, 0x88, 0x17, 0x1e, 0xf9, 0x08, 0xbc, 0xf1, 0xce, 0x03, 0x8f, 0x7c, 0x04,
|
||||
0x94, 0x47, 0x3e, 0x05, 0x9a, 0xd9, 0xdb, 0x78, 0xed, 0xe4, 0xc5, 0xbd, 0xbc, 0xd8, 0x33, 0x67,
|
||||
0xce, 0x6d, 0xce, 0xf9, 0x9d, 0x33, 0x67, 0xe1, 0xdd, 0x88, 0xb0, 0x90, 0x93, 0x68, 0x42, 0xa2,
|
||||
0x8e, 0x5a, 0x52, 0x11, 0x46, 0x67, 0xda, 0xd2, 0x66, 0x51, 0x28, 0x42, 0x04, 0x39, 0xa5, 0x79,
|
||||
0xd3, 0x0b, 0xbd, 0x50, 0x91, 0x3b, 0x72, 0x15, 0x73, 0x34, 0xd7, 0xbd, 0x30, 0xf4, 0x7c, 0xd2,
|
||||
0xc1, 0x8c, 0x76, 0x70, 0x10, 0x84, 0x02, 0x0b, 0x1a, 0x06, 0x3c, 0x39, 0xb5, 0x86, 0xdb, 0xdc,
|
||||
0xa6, 0xa1, 0x3a, 0x75, 0xc3, 0x88, 0x74, 0x26, 0x9b, 0x1d, 0x8f, 0x04, 0x24, 0xc2, 0x82, 0xf4,
|
||||
0x13, 0x9e, 0xcf, 0x3c, 0x2a, 0x06, 0xe3, 0x13, 0xdb, 0x0d, 0x47, 0x1d, 0x1c, 0x29, 0x13, 0xdf,
|
||||
0xaa, 0xc5, 0x5d, 0xb7, 0xdf, 0x61, 0x43, 0x4f, 0x0a, 0xf3, 0x0e, 0x66, 0xcc, 0xa7, 0xae, 0x52,
|
||||
0xde, 0x99, 0x6c, 0x62, 0x9f, 0x0d, 0xf0, 0x8c, 0x2a, 0xeb, 0xa7, 0x2a, 0x5c, 0x3f, 0xc4, 0x01,
|
||||
0x3d, 0x25, 0x5c, 0x38, 0xe4, 0xbb, 0x31, 0xe1, 0x02, 0x7d, 0x09, 0x15, 0x79, 0x09, 0xd3, 0x68,
|
||||
0x1b, 0x1b, 0xf5, 0xad, 0x3d, 0x3b, 0xb7, 0x66, 0xa7, 0xd6, 0xd4, 0xe2, 0x99, 0xdb, 0xb7, 0xd9,
|
||||
0xd0, 0xb3, 0xa5, 0x35, 0x5b, 0xb3, 0x66, 0xa7, 0xd6, 0x6c, 0x27, 0x8b, 0x85, 0xa3, 0x54, 0xa2,
|
||||
0x26, 0xac, 0x46, 0x64, 0x42, 0x39, 0x0d, 0x03, 0xb3, 0xd4, 0x36, 0x36, 0x6a, 0x4e, 0xb6, 0x47,
|
||||
0x26, 0xac, 0x04, 0xe1, 0x0e, 0x76, 0x07, 0xc4, 0x2c, 0xb7, 0x8d, 0x8d, 0x55, 0x27, 0xdd, 0xa2,
|
||||
0x36, 0xd4, 0x31, 0x63, 0x07, 0xf8, 0x84, 0xf8, 0x3d, 0x72, 0x66, 0x56, 0x94, 0xa0, 0x4e, 0x42,
|
||||
0x6f, 0xc3, 0xd5, 0x74, 0xfb, 0x14, 0xfb, 0x63, 0x62, 0x2e, 0x2b, 0x9e, 0x69, 0x22, 0x5a, 0x87,
|
||||
0x5a, 0x80, 0x47, 0x84, 0x33, 0xec, 0x12, 0x73, 0x55, 0x71, 0xe4, 0x04, 0xf4, 0x1c, 0xd6, 0xb4,
|
||||
0x4b, 0x1c, 0x87, 0xe3, 0xc8, 0x25, 0x26, 0xa8, 0x18, 0x1c, 0x2c, 0x10, 0x83, 0x6e, 0x51, 0xa7,
|
||||
0x33, 0x6b, 0x06, 0x7d, 0x0d, 0xcb, 0x0a, 0x37, 0x66, 0xbd, 0x5d, 0x7e, 0x71, 0x31, 0x8f, 0x75,
|
||||
0xa2, 0x21, 0xac, 0x30, 0x7f, 0xec, 0xd1, 0x80, 0x9b, 0x57, 0x94, 0xfa, 0x27, 0x0b, 0xa8, 0xdf,
|
||||
0x09, 0x83, 0x53, 0xea, 0x1d, 0xe2, 0x00, 0x7b, 0x64, 0x44, 0x02, 0x71, 0xa4, 0x34, 0x3b, 0xa9,
|
||||
0x05, 0xf4, 0x3d, 0x34, 0x86, 0x63, 0x2e, 0xc2, 0x11, 0x7d, 0x4e, 0x1e, 0x33, 0x85, 0x6c, 0xf3,
|
||||
0xaa, 0x0a, 0x62, 0x6f, 0x01, 0xab, 0xbd, 0x82, 0x4a, 0x67, 0xc6, 0x88, 0x04, 0xc9, 0x70, 0x7c,
|
||||
0x42, 0x9e, 0x92, 0x48, 0xa1, 0xeb, 0x5a, 0x0c, 0x12, 0x8d, 0x14, 0xc3, 0x88, 0x26, 0x3b, 0x6e,
|
||||
0x5e, 0x6f, 0x97, 0x63, 0x18, 0x65, 0x24, 0xeb, 0x37, 0x03, 0x1a, 0x79, 0x35, 0x70, 0x16, 0x06,
|
||||
0x5c, 0xa1, 0x66, 0x94, 0xd0, 0xb8, 0x69, 0x28, 0xa1, 0x9c, 0x30, 0x8d, 0xa9, 0x52, 0x11, 0x53,
|
||||
0xb7, 0xa0, 0x1a, 0xf7, 0x0c, 0x05, 0xe9, 0x9a, 0x93, 0xec, 0xa6, 0xea, 0xa0, 0x52, 0xa8, 0x83,
|
||||
0x16, 0x00, 0x57, 0xa8, 0xf8, 0xfc, 0x8c, 0x11, 0xb3, 0xaa, 0x4e, 0x35, 0x8a, 0xf5, 0xa3, 0x01,
|
||||
0xd7, 0x0f, 0x28, 0x17, 0x5d, 0xc6, 0xf8, 0xeb, 0x2d, 0x59, 0x6b, 0x0c, 0x2b, 0x5d, 0xc6, 0xa4,
|
||||
0x33, 0x68, 0x13, 0x2a, 0x98, 0xb1, 0x38, 0x40, 0xf5, 0xad, 0xdb, 0xb6, 0xd6, 0x18, 0x13, 0x16,
|
||||
0xf9, 0xcf, 0xf7, 0x02, 0x21, 0x35, 0x4b, 0xd6, 0xe6, 0xc7, 0x50, 0xcb, 0x48, 0xa8, 0x01, 0xe5,
|
||||
0x21, 0x39, 0x53, 0x17, 0xa8, 0x39, 0x72, 0x89, 0x6e, 0xc2, 0xf2, 0x44, 0xd5, 0x72, 0x6c, 0x35,
|
||||
0xde, 0xdc, 0x2f, 0x6d, 0x1b, 0xd6, 0xef, 0x65, 0x78, 0x43, 0xfa, 0x79, 0xac, 0x82, 0xd9, 0x65,
|
||||
0x6c, 0x97, 0x08, 0x4c, 0x7d, 0xfe, 0x64, 0x4c, 0xa2, 0xb3, 0x97, 0x19, 0x8b, 0x3e, 0x54, 0xe3,
|
||||
0x44, 0x28, 0x9f, 0x5e, 0x74, 0x5f, 0x48, 0x74, 0xe7, 0xcd, 0xa0, 0xfc, 0x12, 0x9a, 0xc1, 0xbc,
|
||||
0xfa, 0xac, 0xbc, 0x82, 0xfa, 0xb4, 0x7e, 0x28, 0xc1, 0x2d, 0xe9, 0x4e, 0x9e, 0xae, 0xac, 0xc2,
|
||||
0x10, 0x54, 0x84, 0xc4, 0x7a, 0x9c, 0x7c, 0xb5, 0x46, 0xf7, 0x60, 0x65, 0xc8, 0xc3, 0x20, 0x20,
|
||||
0x22, 0x89, 0x75, 0x53, 0x87, 0x54, 0x2f, 0x3e, 0xea, 0x32, 0x76, 0xcc, 0x88, 0xeb, 0xa4, 0xac,
|
||||
0xe8, 0x0e, 0x54, 0x06, 0xc4, 0x1f, 0xa9, 0x6a, 0xab, 0x6f, 0xfd, 0x5f, 0x17, 0x79, 0x44, 0xfc,
|
||||
0x51, 0xca, 0xaf, 0x98, 0xd0, 0x7d, 0xa8, 0x65, 0x5e, 0x26, 0x31, 0x58, 0x9f, 0x32, 0x92, 0x1e,
|
||||
0xa6, 0x62, 0x39, 0xbb, 0x94, 0xed, 0xd3, 0x88, 0xb8, 0x92, 0x51, 0x3d, 0x36, 0x05, 0xd9, 0xdd,
|
||||
0xf4, 0x30, 0x93, 0xcd, 0xd8, 0xad, 0x5f, 0x0d, 0x78, 0x2b, 0x87, 0xaf, 0x93, 0x14, 0xd3, 0x21,
|
||||
0x11, 0xb8, 0x8f, 0x05, 0x7e, 0xcd, 0x25, 0xfd, 0x67, 0x09, 0xae, 0x4d, 0x47, 0x57, 0xa6, 0x47,
|
||||
0x76, 0xb4, 0x34, 0x3d, 0x72, 0x8d, 0x8e, 0xe0, 0x0a, 0x09, 0x26, 0x34, 0x0a, 0x03, 0xf9, 0x08,
|
||||
0xa4, 0x50, 0xfd, 0xe0, 0xe2, 0x1c, 0xd9, 0x7b, 0x1a, 0x7b, 0xdc, 0x05, 0xa6, 0x34, 0xa0, 0x21,
|
||||
0x00, 0xc3, 0x11, 0x1e, 0x11, 0x41, 0x22, 0x09, 0xc9, 0xf2, 0xa2, 0x90, 0x8c, 0xcd, 0x1f, 0xa5,
|
||||
0x3a, 0x1d, 0x4d, 0x7d, 0xf3, 0x19, 0xac, 0xcd, 0xf8, 0x33, 0xa7, 0x05, 0xdd, 0xd3, 0x5b, 0x50,
|
||||
0x7d, 0xab, 0x35, 0xe7, 0x7a, 0x9a, 0x1a, 0xbd, 0x45, 0xfd, 0x51, 0x82, 0xba, 0x86, 0xb8, 0xb9,
|
||||
0x31, 0x6c, 0x01, 0x28, 0x81, 0x87, 0xd4, 0x27, 0x71, 0x04, 0x6b, 0x8e, 0x46, 0x41, 0x83, 0x39,
|
||||
0x11, 0x79, 0xb4, 0x40, 0x44, 0xa4, 0x3f, 0x73, 0xc3, 0x21, 0x9f, 0x29, 0x65, 0x97, 0x27, 0x73,
|
||||
0x53, 0xb2, 0x43, 0x02, 0xae, 0x9d, 0x52, 0x9f, 0x1c, 0xe5, 0x5e, 0x54, 0x95, 0x17, 0x07, 0x0b,
|
||||
0x7a, 0xf1, 0x50, 0x57, 0xea, 0x14, 0x6c, 0x58, 0xef, 0x43, 0xa3, 0x58, 0x7a, 0xd2, 0x43, 0x3a,
|
||||
0xc2, 0x5e, 0x16, 0xa7, 0x64, 0x67, 0xfd, 0x62, 0x00, 0x9a, 0xcd, 0xc4, 0x45, 0xe1, 0x1e, 0x6e,
|
||||
0xf3, 0x74, 0x3e, 0x88, 0x71, 0xaf, 0x51, 0x50, 0x0f, 0xea, 0x7d, 0xc2, 0x05, 0x0d, 0x94, 0xc3,
|
||||
0x49, 0x43, 0x78, 0xef, 0xf2, 0x94, 0xef, 0xe6, 0x02, 0x8e, 0x2e, 0x6d, 0x7d, 0x01, 0xb7, 0x2f,
|
||||
0xe5, 0xd6, 0x26, 0x03, 0x63, 0x6a, 0x32, 0xb8, 0x74, 0x9e, 0xb0, 0x10, 0x34, 0x8a, 0x9d, 0xc5,
|
||||
0x0a, 0x60, 0x4d, 0xc6, 0x74, 0x67, 0x80, 0x23, 0xf1, 0x0a, 0x06, 0x02, 0xeb, 0x13, 0xa8, 0x65,
|
||||
0xf6, 0xe6, 0x06, 0xba, 0x09, 0xab, 0x93, 0x74, 0xc8, 0x2a, 0xa9, 0x6c, 0x65, 0x7b, 0xab, 0x0b,
|
||||
0x48, 0x77, 0x36, 0x79, 0x00, 0xee, 0xc0, 0x32, 0x15, 0x64, 0x94, 0x4e, 0x0f, 0xff, 0x2b, 0xf6,
|
||||
0x6d, 0xc5, 0xee, 0xc4, 0x3c, 0x5b, 0xff, 0x96, 0x61, 0x2d, 0x6f, 0x9f, 0xf2, 0x97, 0xba, 0x04,
|
||||
0x3d, 0x86, 0xc6, 0x7e, 0xf2, 0x6d, 0x93, 0x4e, 0x70, 0xe8, 0x4d, 0x5d, 0x4f, 0xe1, 0x2b, 0xa7,
|
||||
0xb9, 0x3e, 0xff, 0x30, 0xf6, 0xc8, 0x5a, 0x42, 0x0f, 0x60, 0x35, 0x9d, 0xb2, 0xa6, 0x15, 0x15,
|
||||
0x66, 0xaf, 0xe6, 0x8d, 0x39, 0xb3, 0x8e, 0xb5, 0x84, 0xbe, 0x81, 0xab, 0xfb, 0xaa, 0xfb, 0x25,
|
||||
0xaf, 0x1d, 0x7a, 0x47, 0xe7, 0xbb, 0x70, 0x7c, 0x69, 0x5a, 0x45, 0xb6, 0xd9, 0x07, 0xd3, 0x5a,
|
||||
0x42, 0x3f, 0x1b, 0x70, 0x63, 0x9f, 0x88, 0xe2, 0xe3, 0x81, 0xee, 0xce, 0x37, 0x72, 0xc1, 0x23,
|
||||
0xd3, 0xec, 0x2d, 0x04, 0x8c, 0x69, 0x9d, 0xd6, 0x12, 0x3a, 0x52, 0x77, 0xce, 0x13, 0x8c, 0x6e,
|
||||
0xcf, 0xcd, 0x64, 0x16, 0xba, 0xd6, 0x45, 0xc7, 0xe9, 0x3d, 0x3f, 0x7d, 0xf0, 0xd7, 0x79, 0xcb,
|
||||
0xf8, 0xfb, 0xbc, 0x65, 0xfc, 0x73, 0xde, 0x32, 0xbe, 0xfa, 0xf0, 0xb2, 0x0f, 0x5f, 0xed, 0x03,
|
||||
0x1d, 0x33, 0xea, 0xfa, 0x94, 0x04, 0xe2, 0xa4, 0xaa, 0x3e, 0x73, 0x3f, 0xfa, 0x2f, 0x00, 0x00,
|
||||
0xff, 0xff, 0x61, 0xf3, 0xc7, 0x8e, 0xbf, 0x0f, 0x00, 0x00,
|
||||
// 1230 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x57, 0xdd, 0x6e, 0x1b, 0xc5,
|
||||
0x17, 0xcf, 0xda, 0x8e, 0x13, 0x1f, 0xb7, 0x8d, 0x33, 0xed, 0xbf, 0xff, 0xc5, 0xa4, 0x96, 0x59,
|
||||
0x01, 0x0a, 0x94, 0xae, 0x69, 0xa8, 0x44, 0x55, 0xa4, 0x4a, 0xa6, 0x49, 0x53, 0xe4, 0x44, 0x4d,
|
||||
0x37, 0x50, 0x89, 0x0f, 0xa9, 0x9a, 0xac, 0x27, 0xeb, 0xc1, 0xeb, 0xdd, 0x61, 0x67, 0x6c, 0xe4,
|
||||
0xbe, 0x00, 0xdc, 0x23, 0x6e, 0x78, 0x0c, 0x24, 0x2e, 0xb8, 0x47, 0x88, 0x4b, 0x1e, 0x01, 0xe5,
|
||||
0x92, 0xa7, 0x40, 0x33, 0xfb, 0x35, 0x5e, 0x3b, 0xb9, 0x71, 0x3f, 0x6e, 0xec, 0x99, 0x33, 0x67,
|
||||
0xce, 0x39, 0xf3, 0x9b, 0xdf, 0x39, 0x73, 0x16, 0xde, 0x8d, 0x08, 0x0b, 0x39, 0x89, 0x26, 0x24,
|
||||
0xea, 0xa8, 0x21, 0x15, 0x61, 0x34, 0xd5, 0x86, 0x36, 0x8b, 0x42, 0x11, 0x22, 0xc8, 0x25, 0xcd,
|
||||
0x6b, 0x5e, 0xe8, 0x85, 0x4a, 0xdc, 0x91, 0xa3, 0x58, 0xa3, 0xb9, 0xe5, 0x85, 0xa1, 0xe7, 0x93,
|
||||
0x0e, 0x66, 0xb4, 0x83, 0x83, 0x20, 0x14, 0x58, 0xd0, 0x30, 0xe0, 0xc9, 0xaa, 0x35, 0xbc, 0xcb,
|
||||
0x6d, 0x1a, 0xaa, 0x55, 0x37, 0x8c, 0x48, 0x67, 0x72, 0xbb, 0xe3, 0x91, 0x80, 0x44, 0x58, 0x90,
|
||||
0x7e, 0xa2, 0xf3, 0x99, 0x47, 0xc5, 0x60, 0x7c, 0x62, 0xbb, 0xe1, 0xa8, 0x83, 0x23, 0xe5, 0xe2,
|
||||
0x5b, 0x35, 0xb8, 0xe5, 0xf6, 0x3b, 0x6c, 0xe8, 0xc9, 0xcd, 0xbc, 0x83, 0x19, 0xf3, 0xa9, 0xab,
|
||||
0x8c, 0x77, 0x26, 0xb7, 0xb1, 0xcf, 0x06, 0x78, 0xce, 0x94, 0xf5, 0x5b, 0x15, 0x36, 0x0e, 0x71,
|
||||
0x40, 0x4f, 0x09, 0x17, 0x0e, 0xf9, 0x6e, 0x4c, 0xb8, 0x40, 0x5f, 0x42, 0x45, 0x1e, 0xc2, 0x34,
|
||||
0xda, 0xc6, 0x76, 0x7d, 0x67, 0xcf, 0xce, 0xbd, 0xd9, 0xa9, 0x37, 0x35, 0x78, 0xe6, 0xf6, 0x6d,
|
||||
0x36, 0xf4, 0x6c, 0xe9, 0xcd, 0xd6, 0xbc, 0xd9, 0xa9, 0x37, 0xdb, 0xc9, 0xb0, 0x70, 0x94, 0x49,
|
||||
0xd4, 0x84, 0xf5, 0x88, 0x4c, 0x28, 0xa7, 0x61, 0x60, 0x96, 0xda, 0xc6, 0x76, 0xcd, 0xc9, 0xe6,
|
||||
0xc8, 0x84, 0xb5, 0x20, 0x7c, 0x80, 0xdd, 0x01, 0x31, 0xcb, 0x6d, 0x63, 0x7b, 0xdd, 0x49, 0xa7,
|
||||
0xa8, 0x0d, 0x75, 0xcc, 0xd8, 0x01, 0x3e, 0x21, 0x7e, 0x8f, 0x4c, 0xcd, 0x8a, 0xda, 0xa8, 0x8b,
|
||||
0xd0, 0xdb, 0x70, 0x39, 0x9d, 0x3e, 0xc5, 0xfe, 0x98, 0x98, 0xab, 0x4a, 0x67, 0x56, 0x88, 0xb6,
|
||||
0xa0, 0x16, 0xe0, 0x11, 0xe1, 0x0c, 0xbb, 0xc4, 0x5c, 0x57, 0x1a, 0xb9, 0x00, 0x3d, 0x87, 0x4d,
|
||||
0xed, 0x10, 0xc7, 0xe1, 0x38, 0x72, 0x89, 0x09, 0x0a, 0x83, 0x83, 0x25, 0x30, 0xe8, 0x16, 0x6d,
|
||||
0x3a, 0xf3, 0x6e, 0xd0, 0xd7, 0xb0, 0xaa, 0x78, 0x63, 0xd6, 0xdb, 0xe5, 0x17, 0x87, 0x79, 0x6c,
|
||||
0x13, 0x0d, 0x61, 0x8d, 0xf9, 0x63, 0x8f, 0x06, 0xdc, 0xbc, 0xa4, 0xcc, 0x3f, 0x59, 0xc2, 0xfc,
|
||||
0x83, 0x30, 0x38, 0xa5, 0xde, 0x21, 0x0e, 0xb0, 0x47, 0x46, 0x24, 0x10, 0x47, 0xca, 0xb2, 0x93,
|
||||
0x7a, 0x40, 0xdf, 0x43, 0x63, 0x38, 0xe6, 0x22, 0x1c, 0xd1, 0xe7, 0xe4, 0x31, 0x53, 0xcc, 0x36,
|
||||
0x2f, 0x2b, 0x10, 0x7b, 0x4b, 0x78, 0xed, 0x15, 0x4c, 0x3a, 0x73, 0x4e, 0x24, 0x49, 0x86, 0xe3,
|
||||
0x13, 0xf2, 0x94, 0x44, 0x8a, 0x5d, 0x57, 0x62, 0x92, 0x68, 0xa2, 0x98, 0x46, 0x34, 0x99, 0x71,
|
||||
0x73, 0xa3, 0x5d, 0x8e, 0x69, 0x94, 0x89, 0xd0, 0x36, 0x6c, 0x4c, 0x48, 0x44, 0x4f, 0xa7, 0xc7,
|
||||
0xd4, 0x0b, 0xb0, 0x18, 0x47, 0xc4, 0x6c, 0x28, 0x2a, 0x16, 0xc5, 0xd6, 0x9f, 0x06, 0x34, 0xf2,
|
||||
0xbc, 0xe1, 0x2c, 0x0c, 0xb8, 0xe2, 0xd7, 0x28, 0x91, 0x71, 0xd3, 0x50, 0xe6, 0x73, 0xc1, 0x2c,
|
||||
0xfb, 0x4a, 0x45, 0xf6, 0x5d, 0x87, 0x6a, 0x5c, 0x5d, 0x14, 0xf9, 0x6b, 0x4e, 0x32, 0x9b, 0xc9,
|
||||
0x98, 0x4a, 0x21, 0x63, 0x5a, 0x00, 0x5c, 0xf1, 0xe7, 0xf3, 0x29, 0x23, 0x66, 0x55, 0xad, 0x6a,
|
||||
0x12, 0x64, 0xc1, 0xa5, 0x38, 0x6e, 0x87, 0xf0, 0xb1, 0x2f, 0xcc, 0x35, 0xa5, 0x31, 0x23, 0xb3,
|
||||
0x7e, 0x34, 0x60, 0xe3, 0x80, 0x72, 0xd1, 0x65, 0x8c, 0xbf, 0xde, 0x02, 0x60, 0x8d, 0x61, 0xad,
|
||||
0xcb, 0x98, 0x0c, 0x06, 0xdd, 0x86, 0x0a, 0x66, 0x2c, 0x06, 0xb1, 0xbe, 0x73, 0xc3, 0xd6, 0xca,
|
||||
0x6c, 0xa2, 0x22, 0xff, 0xf9, 0x5e, 0x20, 0xa4, 0x65, 0xa9, 0xda, 0xfc, 0x18, 0x6a, 0x99, 0x08,
|
||||
0x35, 0xa0, 0x3c, 0x24, 0x53, 0x75, 0x80, 0x9a, 0x23, 0x87, 0xe8, 0x1a, 0xac, 0x4e, 0x54, 0x65,
|
||||
0x88, 0xbd, 0xc6, 0x93, 0x7b, 0xa5, 0xbb, 0x86, 0xf5, 0x6b, 0x19, 0xde, 0x90, 0x71, 0x1e, 0x2b,
|
||||
0xc0, 0xbb, 0x8c, 0xed, 0x12, 0x81, 0xa9, 0xcf, 0x9f, 0x8c, 0x49, 0x34, 0x7d, 0x99, 0x58, 0xf4,
|
||||
0xa1, 0x1a, 0x5f, 0x96, 0x8a, 0xe9, 0x45, 0x57, 0x99, 0xc4, 0x76, 0x5e, 0x5a, 0xca, 0x2f, 0xa1,
|
||||
0xb4, 0x2c, 0xca, 0xf6, 0xca, 0x2b, 0xc8, 0x76, 0xeb, 0x87, 0x12, 0x5c, 0x97, 0xe1, 0xe4, 0xd7,
|
||||
0x95, 0x65, 0x21, 0x82, 0x8a, 0x90, 0xf9, 0x10, 0x5f, 0xbe, 0x1a, 0xa3, 0x3b, 0xb0, 0x36, 0xe4,
|
||||
0x61, 0x10, 0x10, 0x91, 0x60, 0xdd, 0xd4, 0x29, 0xd5, 0x8b, 0x97, 0xba, 0x8c, 0x1d, 0x33, 0xe2,
|
||||
0x3a, 0xa9, 0x2a, 0xba, 0x09, 0x95, 0x01, 0xf1, 0x47, 0x2a, 0x23, 0xeb, 0x3b, 0xff, 0xd7, 0xb7,
|
||||
0x3c, 0x22, 0xfe, 0x28, 0xd5, 0x57, 0x4a, 0xe8, 0x1e, 0xd4, 0xb2, 0x28, 0x13, 0x0c, 0xb6, 0x66,
|
||||
0x9c, 0xa4, 0x8b, 0xe9, 0xb6, 0x5c, 0x5d, 0xee, 0xed, 0xd3, 0x88, 0xb8, 0x52, 0x51, 0x3d, 0x5d,
|
||||
0x85, 0xbd, 0xbb, 0xe9, 0x62, 0xb6, 0x37, 0x53, 0xb7, 0x7e, 0x31, 0xe0, 0xad, 0x9c, 0xbe, 0x4e,
|
||||
0x92, 0x4c, 0x87, 0x44, 0xe0, 0x3e, 0x16, 0xf8, 0x35, 0xa7, 0xf4, 0x1f, 0x25, 0xb8, 0x32, 0x8b,
|
||||
0xae, 0xbc, 0x1e, 0x59, 0xf5, 0xd2, 0xeb, 0x91, 0x63, 0x74, 0x04, 0x97, 0x48, 0x30, 0xa1, 0x51,
|
||||
0x18, 0xc8, 0x27, 0x25, 0xa5, 0xea, 0x07, 0xe7, 0xdf, 0x91, 0xbd, 0xa7, 0xa9, 0xc7, 0x55, 0x60,
|
||||
0xc6, 0x02, 0x1a, 0x02, 0x30, 0x1c, 0xe1, 0x11, 0x11, 0x24, 0x92, 0x94, 0x2c, 0x2f, 0x4b, 0xc9,
|
||||
0xd8, 0xfd, 0x51, 0x6a, 0xd3, 0xd1, 0xcc, 0x37, 0x9f, 0xc1, 0xe6, 0x5c, 0x3c, 0x0b, 0x4a, 0xd0,
|
||||
0x1d, 0xbd, 0x04, 0xd5, 0x77, 0x5a, 0x0b, 0x8e, 0xa7, 0x99, 0xd1, 0x4b, 0xd4, 0xef, 0x25, 0xa8,
|
||||
0x6b, 0x8c, 0x5b, 0x88, 0x61, 0x0b, 0x40, 0x6d, 0x78, 0x48, 0x7d, 0x12, 0x23, 0x58, 0x73, 0x34,
|
||||
0x09, 0x1a, 0x2c, 0x40, 0xe4, 0xd1, 0x12, 0x88, 0xc8, 0x78, 0x16, 0xc2, 0x21, 0x9f, 0x32, 0xe5,
|
||||
0x97, 0x27, 0x5d, 0x58, 0x32, 0x43, 0x02, 0xae, 0x9c, 0x52, 0x9f, 0x1c, 0xe5, 0x51, 0x54, 0x55,
|
||||
0x14, 0x07, 0x4b, 0x46, 0xf1, 0x50, 0x37, 0xea, 0x14, 0x7c, 0x58, 0xef, 0x43, 0xa3, 0x98, 0x7a,
|
||||
0x32, 0x42, 0x3a, 0xc2, 0x5e, 0x86, 0x53, 0x32, 0xb3, 0x7e, 0x36, 0x00, 0xcd, 0xdf, 0xc4, 0x79,
|
||||
0x70, 0x0f, 0xef, 0xf2, 0xb4, 0xdb, 0x88, 0x79, 0xaf, 0x49, 0x50, 0x0f, 0xea, 0x7d, 0xc2, 0x05,
|
||||
0x0d, 0x54, 0xc0, 0x49, 0x41, 0x78, 0xef, 0xe2, 0x2b, 0xdf, 0xcd, 0x37, 0x38, 0xfa, 0x6e, 0xeb,
|
||||
0x0b, 0xb8, 0x71, 0xa1, 0xb6, 0xd6, 0x3d, 0x18, 0x33, 0xdd, 0xc3, 0x85, 0x3d, 0x87, 0x85, 0xa0,
|
||||
0x51, 0xac, 0x2c, 0x56, 0x00, 0x9b, 0x12, 0xd3, 0x07, 0x03, 0x1c, 0x89, 0x57, 0xd0, 0x10, 0x58,
|
||||
0x9f, 0x40, 0x2d, 0xf3, 0xb7, 0x10, 0xe8, 0x26, 0xac, 0x4f, 0xd2, 0x96, 0xad, 0xa4, 0x6e, 0x2b,
|
||||
0x9b, 0x5b, 0x5d, 0x40, 0x7a, 0xb0, 0xc9, 0x03, 0x70, 0x13, 0x56, 0xa9, 0x20, 0xa3, 0xb4, 0x7b,
|
||||
0xf8, 0x5f, 0xb1, 0x6e, 0x2b, 0x75, 0x27, 0xd6, 0xd9, 0xf9, 0xb7, 0x0c, 0x9b, 0x79, 0xf9, 0x94,
|
||||
0xbf, 0xd4, 0x25, 0xe8, 0x31, 0x34, 0xf6, 0x93, 0x2f, 0xa5, 0xb4, 0xcb, 0x43, 0x6f, 0xea, 0x76,
|
||||
0x0a, 0xdf, 0x4c, 0xcd, 0xad, 0xc5, 0x8b, 0x71, 0x44, 0xd6, 0x0a, 0xba, 0x0f, 0xeb, 0x69, 0x97,
|
||||
0x35, 0x6b, 0xa8, 0xd0, 0x7b, 0x35, 0xaf, 0x2e, 0xe8, 0x75, 0xac, 0x15, 0xf4, 0x0d, 0x5c, 0xde,
|
||||
0x57, 0xd5, 0x2f, 0x79, 0xed, 0xd0, 0x3b, 0xba, 0xde, 0xb9, 0xed, 0x4b, 0xd3, 0x2a, 0xaa, 0xcd,
|
||||
0x3f, 0x98, 0xd6, 0x0a, 0xfa, 0xc9, 0x80, 0xab, 0xfb, 0x44, 0x14, 0x1f, 0x0f, 0x74, 0x6b, 0xb1,
|
||||
0x93, 0x73, 0x1e, 0x99, 0x66, 0x6f, 0x29, 0x62, 0xcc, 0xda, 0xb4, 0x56, 0xd0, 0x91, 0x3a, 0x73,
|
||||
0x7e, 0xc1, 0xe8, 0xc6, 0xc2, 0x9b, 0xcc, 0xa0, 0x6b, 0x9d, 0xb7, 0x9c, 0x9e, 0xf3, 0xd3, 0xfb,
|
||||
0x7f, 0x9d, 0xb5, 0x8c, 0xbf, 0xcf, 0x5a, 0xc6, 0x3f, 0x67, 0x2d, 0xe3, 0xab, 0x0f, 0x2f, 0xfa,
|
||||
0x8c, 0xd6, 0x3e, 0xf7, 0x31, 0xa3, 0xae, 0x4f, 0x49, 0x20, 0x4e, 0xaa, 0xea, 0xa3, 0xf9, 0xa3,
|
||||
0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0x2d, 0x37, 0xe2, 0x11, 0x0d, 0x10, 0x00, 0x00,
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
|
|
@ -1431,6 +1451,18 @@ func (m *ManifestRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|||
i -= len(m.XXX_unrecognized)
|
||||
copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
if m.VerifySignature {
|
||||
i--
|
||||
if m.VerifySignature {
|
||||
dAtA[i] = 1
|
||||
} else {
|
||||
dAtA[i] = 0
|
||||
}
|
||||
i--
|
||||
dAtA[i] = 0x1
|
||||
i--
|
||||
dAtA[i] = 0x80
|
||||
}
|
||||
if len(m.ApiVersions) > 0 {
|
||||
for iNdEx := len(m.ApiVersions) - 1; iNdEx >= 0; iNdEx-- {
|
||||
i -= len(m.ApiVersions[iNdEx])
|
||||
|
|
@ -1576,6 +1608,13 @@ func (m *ManifestResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|||
i -= len(m.XXX_unrecognized)
|
||||
copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
if len(m.VerifyResult) > 0 {
|
||||
i -= len(m.VerifyResult)
|
||||
copy(dAtA[i:], m.VerifyResult)
|
||||
i = encodeVarintRepository(dAtA, i, uint64(len(m.VerifyResult)))
|
||||
i--
|
||||
dAtA[i] = 0x3a
|
||||
}
|
||||
if len(m.SourceType) > 0 {
|
||||
i -= len(m.SourceType)
|
||||
copy(dAtA[i:], m.SourceType)
|
||||
|
|
@ -2415,6 +2454,9 @@ func (m *ManifestRequest) Size() (n int) {
|
|||
n += 1 + l + sovRepository(uint64(l))
|
||||
}
|
||||
}
|
||||
if m.VerifySignature {
|
||||
n += 3
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
|
|
@ -2449,6 +2491,10 @@ func (m *ManifestResponse) Size() (n int) {
|
|||
if l > 0 {
|
||||
n += 1 + l + sovRepository(uint64(l))
|
||||
}
|
||||
l = len(m.VerifyResult)
|
||||
if l > 0 {
|
||||
n += 1 + l + sovRepository(uint64(l))
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
|
|
@ -3203,6 +3249,26 @@ 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)
|
||||
}
|
||||
var v int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowRepository
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
v |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
m.VerifySignature = bool(v != 0)
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipRepository(dAtA[iNdEx:])
|
||||
|
|
@ -3417,6 +3483,38 @@ func (m *ManifestResponse) Unmarshal(dAtA []byte) error {
|
|||
}
|
||||
m.SourceType = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
case 7:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field VerifyResult", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowRepository
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthRepository
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.VerifyResult = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipRepository(dAtA[iNdEx:])
|
||||
|
|
|
|||
71
reposerver/gpgwatcher.go
Normal file
71
reposerver/gpgwatcher.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package reposerver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
)
|
||||
|
||||
// StartGPGWatcher watches a given directory for creation and deletion of files and syncs the GPG keyring
|
||||
func StartGPGWatcher(sourcePath string) error {
|
||||
log.Infof("Starting GPG sync watcher on directory '%s'", sourcePath)
|
||||
forceSync := false
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Remove == fsnotify.Remove {
|
||||
// In case our watched path is re-created (i.e. during e2e tests), we need to watch again
|
||||
if event.Name == sourcePath && event.Op&fsnotify.Remove == fsnotify.Remove {
|
||||
log.Warnf("Re-creating watcher on %s", sourcePath)
|
||||
time.Sleep(1 * time.Second)
|
||||
err = watcher.Add(sourcePath)
|
||||
if err != nil {
|
||||
log.Errorf("Error re-creating watcher on %s: %v", sourcePath, err)
|
||||
return
|
||||
}
|
||||
// Force sync because we probably missed an event
|
||||
forceSync = true
|
||||
}
|
||||
if gpg.IsShortKeyID(path.Base(event.Name)) || forceSync {
|
||||
log.Infof("Updating GPG keyring on filesystem event")
|
||||
added, removed, err := gpg.SyncKeyRingFromDirectory(sourcePath)
|
||||
if err != nil {
|
||||
log.Errorf("Could not sync keyring: %s", err.Error())
|
||||
} else {
|
||||
log.Infof("Result of sync operation: keys added: %d, keys removed: %d", len(added), len(removed))
|
||||
}
|
||||
forceSync = false
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Errorf("%v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = watcher.Add(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
<-done
|
||||
return fmt.Errorf("Abnormal termination of GPG watcher, refusing to continue.")
|
||||
}
|
||||
|
|
@ -53,3 +53,7 @@ func (w *gitClientWrapper) Init() error {
|
|||
func (w *gitClientWrapper) RevisionMetadata(revision string) (*git.RevisionMetadata, error) {
|
||||
return w.client.RevisionMetadata(revision)
|
||||
}
|
||||
|
||||
func (w *gitClientWrapper) VerifyCommitSignature(revision string) (string, error) {
|
||||
return w.client.VerifyCommitSignature(revision)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import (
|
|||
"github.com/argoproj/argo-cd/util/app/discovery"
|
||||
argopath "github.com/argoproj/argo-cd/util/app/path"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
"github.com/argoproj/argo-cd/util/helm"
|
||||
"github.com/argoproj/argo-cd/util/ksonnet"
|
||||
argokube "github.com/argoproj/argo-cd/util/kube"
|
||||
|
|
@ -118,13 +119,15 @@ func (s *Service) runRepoOperation(
|
|||
revision string,
|
||||
repo *v1alpha1.Repository,
|
||||
source *v1alpha1.ApplicationSource,
|
||||
verifyCommit bool,
|
||||
getCached func(revision string) bool,
|
||||
operation func(appPath, repoRoot, revision string) error,
|
||||
operation func(appPath, repoRoot, revision, verifyResult string) error,
|
||||
settings operationSettings) error {
|
||||
|
||||
var gitClient git.Client
|
||||
var helmClient helm.Client
|
||||
var err error
|
||||
var signature string
|
||||
revision = textutils.FirstNonEmpty(revision, source.TargetRevision)
|
||||
if source.IsHelm() {
|
||||
helmClient, revision, err = s.newHelmClientResolveRevision(repo, revision, source.Chart)
|
||||
|
|
@ -169,7 +172,7 @@ func (s *Service) runRepoOperation(
|
|||
return err
|
||||
}
|
||||
defer io.Close(closer)
|
||||
return operation(chartPath, chartPath, revision)
|
||||
return operation(chartPath, chartPath, revision, "")
|
||||
} else {
|
||||
s.repoLock.Lock(gitClient.Root())
|
||||
defer s.repoLock.Unlock(gitClient.Root())
|
||||
|
|
@ -181,11 +184,17 @@ func (s *Service) runRepoOperation(
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if verifyCommit {
|
||||
signature, err = gitClient.VerifyCommitSignature(revision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
appPath, err := argopath.Path(gitClient.Root(), source.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return operation(appPath, gitClient.Root(), revision)
|
||||
return operation(appPath, gitClient.Root(), revision, signature)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,13 +214,14 @@ func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestReq
|
|||
}
|
||||
return false
|
||||
}
|
||||
err := s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, getCached, func(appPath, repoRoot, revision string) error {
|
||||
err := s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, q.VerifySignature, getCached, func(appPath, repoRoot, revision, verifyResult string) error {
|
||||
var err error
|
||||
res, err = GenerateManifests(appPath, repoRoot, revision, q, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res.Revision = revision
|
||||
res.VerifyResult = verifyResult
|
||||
err = s.cache.SetManifests(revision, q.ApplicationSource, q.Namespace, q.AppLabelKey, q.AppLabelValue, &res)
|
||||
if err != nil {
|
||||
log.Warnf("manifest cache set error %s/%s: %v", q.ApplicationSource.String(), revision, err)
|
||||
|
|
@ -672,7 +682,7 @@ func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppD
|
|||
return false
|
||||
}
|
||||
|
||||
err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, getCached, func(appPath, repoRoot, revision string) error {
|
||||
err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, false, getCached, func(appPath, repoRoot, revision, verifyResult string) error {
|
||||
appSourceType, err := GetAppSourceType(q.Source, appPath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -810,9 +820,31 @@ func (s *Service) GetRevisionMetadata(ctx context.Context, q *apiclient.RepoServ
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Run gpg verify-commit on the revision
|
||||
signatureInfo := ""
|
||||
if gpg.IsGPGEnabled() {
|
||||
cs, err := gitClient.VerifyCommitSignature(q.Revision)
|
||||
if err != nil {
|
||||
log.Debugf("Could not verify commit signature: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cs != "" {
|
||||
vr, err := gpg.ParseGitCommitVerification(cs)
|
||||
if err != nil {
|
||||
log.Debugf("Could not parse commit verification: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
signatureInfo = fmt.Sprintf("%s signature from %s key %s", vr.Result, vr.Cipher, gpg.KeyID(vr.KeyID))
|
||||
} else {
|
||||
signatureInfo = "Revision is not signed."
|
||||
}
|
||||
}
|
||||
|
||||
// discard anything after the first new line and then truncate to 64 chars
|
||||
message := text.Trunc(strings.SplitN(m.Message, "\n", 2)[0], 64)
|
||||
metadata = &v1alpha1.RevisionMetadata{Author: m.Author, Date: metav1.Time{Time: m.Date}, Tags: m.Tags, Message: message}
|
||||
metadata = &v1alpha1.RevisionMetadata{Author: m.Author, Date: metav1.Time{Time: m.Date}, Tags: m.Tags, Message: message, SignatureInfo: signatureInfo}
|
||||
_ = s.cache.SetRevisionMetadata(q.Repo.Repo, q.Revision, metadata)
|
||||
return metadata, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ message ManifestRequest {
|
|||
github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.KustomizeOptions kustomizeOptions = 13;
|
||||
string kubeVersion = 14;
|
||||
repeated string apiVersions = 15;
|
||||
// Request to verify the signature when generating the manifests (only for Git repositories)
|
||||
bool verifySignature = 16;
|
||||
}
|
||||
|
||||
message ManifestResponse {
|
||||
|
|
@ -32,6 +34,8 @@ message ManifestResponse {
|
|||
// resolved revision
|
||||
string revision = 4;
|
||||
string sourceType = 6;
|
||||
// Raw response of git verify-commit operation (always the empty string for Helm)
|
||||
string verifyResult = 7;
|
||||
}
|
||||
|
||||
// ListAppsRequest requests a repository directory structure
|
||||
|
|
|
|||
|
|
@ -29,7 +29,12 @@ import (
|
|||
helmmocks "github.com/argoproj/argo-cd/util/helm/mocks"
|
||||
)
|
||||
|
||||
func newServiceWithMocks(root string) (*Service, *gitmocks.Client) {
|
||||
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]
|
||||
`
|
||||
|
||||
func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client) {
|
||||
service := NewService(metrics.NewMetricsServer(), cache.NewCache(
|
||||
cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
|
||||
1*time.Minute,
|
||||
|
|
@ -46,6 +51,11 @@ func newServiceWithMocks(root string) (*Service, *gitmocks.Client) {
|
|||
gitClient.On("LsRemote", mock.Anything).Return(mock.Anything, nil)
|
||||
gitClient.On("CommitSHA").Return(mock.Anything, nil)
|
||||
gitClient.On("Root").Return(root)
|
||||
if signed {
|
||||
gitClient.On("VerifyCommitSignature", mock.Anything).Return(testSignature, nil)
|
||||
} else {
|
||||
gitClient.On("VerifyCommitSignature", mock.Anything).Return("", nil)
|
||||
}
|
||||
|
||||
chart := "my-chart"
|
||||
version := semver.MustParse("1.1.0")
|
||||
|
|
@ -65,7 +75,12 @@ func newServiceWithMocks(root string) (*Service, *gitmocks.Client) {
|
|||
}
|
||||
|
||||
func newService(root string) *Service {
|
||||
service, _ := newServiceWithMocks(root)
|
||||
service, _ := newServiceWithMocks(root, false)
|
||||
return service
|
||||
}
|
||||
|
||||
func newServiceWithSignature(root string) *Service {
|
||||
service, _ := newServiceWithMocks(root, true)
|
||||
return service
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +91,7 @@ func TestGenerateYamlManifestInDir(t *testing.T) {
|
|||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src}
|
||||
|
||||
// update this value if we add/remove manifests
|
||||
const countOfManifests = 25
|
||||
const countOfManifests = 26
|
||||
|
||||
res1, err := service.GenerateManifest(context.Background(), &q)
|
||||
|
||||
|
|
@ -107,7 +122,7 @@ func TestHelmManifestFromChartRepo(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateManifestsUseExactRevision(t *testing.T) {
|
||||
service, gitClient := newServiceWithMocks(".")
|
||||
service, gitClient := newServiceWithMocks(".", false)
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
|
||||
|
||||
|
|
@ -568,7 +583,7 @@ func TestGetHelmCharts(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetRevisionMetadata(t *testing.T) {
|
||||
service, gitClient := newServiceWithMocks("../..")
|
||||
service, gitClient := newServiceWithMocks("../..", false)
|
||||
now := time.Now()
|
||||
|
||||
gitClient.On("RevisionMetadata", mock.Anything).Return(&git.RevisionMetadata{
|
||||
|
|
@ -591,6 +606,53 @@ func TestGetRevisionMetadata(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func TestGetSignatureVerificationResult(t *testing.T) {
|
||||
// Commit with signature and verification requested
|
||||
{
|
||||
service := newServiceWithSignature("../..")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "manifests/base"}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true}
|
||||
|
||||
res, err := service.GenerateManifest(context.Background(), &q)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testSignature, res.VerifyResult)
|
||||
}
|
||||
// Commit with signature and verification not requested
|
||||
{
|
||||
service := newServiceWithSignature("../..")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "manifests/base"}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src}
|
||||
|
||||
res, err := service.GenerateManifest(context.Background(), &q)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, res.VerifyResult)
|
||||
}
|
||||
// Commit without signature and verification requested
|
||||
{
|
||||
service := newService("../..")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "manifests/base"}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true}
|
||||
|
||||
res, err := service.GenerateManifest(context.Background(), &q)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, res.VerifyResult)
|
||||
}
|
||||
// Commit without signature and verification not requested
|
||||
{
|
||||
service := newService("../..")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "manifests/base"}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true}
|
||||
|
||||
res, err := service.GenerateManifest(context.Background(), &q)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, res.VerifyResult)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newEnv(t *testing.T) {
|
||||
assert.Equal(t, &argoappv1.Env{
|
||||
&argoappv1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: "my-app-name"},
|
||||
|
|
|
|||
|
|
@ -1070,6 +1070,12 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR
|
|||
if a.Spec.SyncPolicy != nil {
|
||||
syncOptions = a.Spec.SyncPolicy.SyncOptions
|
||||
}
|
||||
|
||||
// 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.")
|
||||
}
|
||||
|
||||
op := appv1.Operation{
|
||||
Sync: &appv1.SyncOperation{
|
||||
Revision: revision,
|
||||
|
|
|
|||
120
server/gpgkey/gpgkey.go
Normal file
120
server/gpgkey/gpgkey.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package gpgkey
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/rbacpolicy"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
"github.com/argoproj/argo-cd/util/rbac"
|
||||
)
|
||||
|
||||
// Server provides a service of type GPGKeyService
|
||||
type Server struct {
|
||||
db db.ArgoDB
|
||||
repoClientset apiclient.Clientset
|
||||
enf *rbac.Enforcer
|
||||
}
|
||||
|
||||
// NewServer returns a new instance of the service with type GPGKeyService
|
||||
func NewServer(
|
||||
repoClientset apiclient.Clientset,
|
||||
db db.ArgoDB,
|
||||
enf *rbac.Enforcer,
|
||||
) *Server {
|
||||
return &Server{
|
||||
db: db,
|
||||
repoClientset: repoClientset,
|
||||
enf: enf,
|
||||
}
|
||||
}
|
||||
|
||||
// ListGnuPGPublicKeys returns a list of GnuPG public keys in the configuration
|
||||
func (s *Server) List(ctx context.Context, q *gpgkeypkg.GnuPGPublicKeyQuery) (*appsv1.GnuPGPublicKeyList, error) {
|
||||
if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceGPGKeys, rbacpolicy.ActionGet, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys, err := s.db.ListConfiguredGPGPublicKeys(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyList := &appsv1.GnuPGPublicKeyList{}
|
||||
for _, v := range keys {
|
||||
// Remove key's data from list result to save some bytes
|
||||
v.KeyData = ""
|
||||
keyList.Items = append(keyList.Items, *v)
|
||||
}
|
||||
return keyList, nil
|
||||
}
|
||||
|
||||
// GetGnuPGPublicKey retrieves a single GPG public key from the configuration
|
||||
func (s *Server) Get(ctx context.Context, q *gpgkeypkg.GnuPGPublicKeyQuery) (*appsv1.GnuPGPublicKey, error) {
|
||||
if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceGPGKeys, rbacpolicy.ActionGet, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyID := gpg.KeyID(q.KeyID)
|
||||
if keyID == "" {
|
||||
return nil, fmt.Errorf("KeyID is malformed or empty")
|
||||
}
|
||||
|
||||
keys, err := s.db.ListConfiguredGPGPublicKeys(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if key, ok := keys[keyID]; ok {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("No such key: %s", keyID)
|
||||
}
|
||||
|
||||
// CreateGnuPGPublicKey adds one or more GPG public keys to the server's configuration
|
||||
func (s *Server) Create(ctx context.Context, q *gpgkeypkg.GnuPGPublicKeyCreateRequest) (*gpgkeypkg.GnuPGPublicKeyCreateResponse, error) {
|
||||
if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceGPGKeys, rbacpolicy.ActionCreate, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyData := strings.TrimSpace(q.Publickey.KeyData)
|
||||
if keyData == "" {
|
||||
return nil, fmt.Errorf("Submitted key data is empty")
|
||||
}
|
||||
|
||||
added, skipped, err := s.db.AddGPGPublicKey(ctx, q.Publickey.KeyData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]appsv1.GnuPGPublicKey, 0)
|
||||
for _, k := range added {
|
||||
items = append(items, *k)
|
||||
}
|
||||
|
||||
response := &gpgkeypkg.GnuPGPublicKeyCreateResponse{
|
||||
Created: &appsv1.GnuPGPublicKeyList{Items: items},
|
||||
Skipped: skipped,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// DeleteGnuPGPublicKey removes a single GPG public key from the server's configuration
|
||||
func (s *Server) Delete(ctx context.Context, q *gpgkeypkg.GnuPGPublicKeyQuery) (*gpgkeypkg.GnuPGPublicKeyResponse, error) {
|
||||
if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceGPGKeys, rbacpolicy.ActionDelete, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err := s.db.DeleteGPGPublicKey(ctx, q.KeyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gpgkeypkg.GnuPGPublicKeyResponse{}, nil
|
||||
}
|
||||
62
server/gpgkey/gpgkey.proto
Normal file
62
server/gpgkey/gpgkey.proto
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
syntax = "proto3";
|
||||
option go_package = "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey";
|
||||
|
||||
// GPG public key service
|
||||
//
|
||||
// GPG public key API performs CRUD actions against GnuPGPublicKey resources
|
||||
package gpgkey;
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
import "google/api/annotations.proto";
|
||||
import "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1/generated.proto";
|
||||
|
||||
// Message to query the server for configured GPG public keys
|
||||
message GnuPGPublicKeyQuery {
|
||||
// The GPG key ID to query for
|
||||
string keyID = 1;
|
||||
}
|
||||
|
||||
// Request to create one or more public keys on the server
|
||||
message GnuPGPublicKeyCreateRequest {
|
||||
// Raw key data of the GPG key(s) to create
|
||||
github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.GnuPGPublicKey publickey = 1;
|
||||
// Whether to upsert already existing public keys
|
||||
bool upsert = 2;
|
||||
}
|
||||
|
||||
// Response to a public key creation request
|
||||
message GnuPGPublicKeyCreateResponse {
|
||||
// List of GPG public keys that have been created
|
||||
github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.GnuPGPublicKeyList created = 1;
|
||||
// List of key IDs that haven been skipped because they already exist on the server
|
||||
repeated string skipped = 2;
|
||||
}
|
||||
|
||||
// Generic (empty) response for GPG public key CRUD requests
|
||||
message GnuPGPublicKeyResponse {}
|
||||
|
||||
// GPGKeyService implements API for managing GPG public keys on the server
|
||||
service GPGKeyService {
|
||||
// List all available repository certificates
|
||||
rpc List(GnuPGPublicKeyQuery) returns (github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.GnuPGPublicKeyList) {
|
||||
option (google.api.http).get = "/api/v1/gpgkeys";
|
||||
}
|
||||
|
||||
// Get information about specified GPG public key from the server
|
||||
rpc Get(GnuPGPublicKeyQuery) returns (github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.GnuPGPublicKey) {
|
||||
option (google.api.http).get = "/api/v1/gpgkeys/{keyID}";
|
||||
}
|
||||
|
||||
// Create one or more GPG public keys in the server's configuration
|
||||
rpc Create(GnuPGPublicKeyCreateRequest) returns (GnuPGPublicKeyCreateResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v1/gpgkeys"
|
||||
body: "publickey"
|
||||
};
|
||||
}
|
||||
|
||||
// Delete specified GPG public key from the server's configuration
|
||||
rpc Delete(GnuPGPublicKeyQuery) returns (GnuPGPublicKeyResponse) {
|
||||
option (google.api.http).delete = "/api/v1/gpgkeys";
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ const (
|
|||
ResourceRepositories = "repositories"
|
||||
ResourceCertificates = "certificates"
|
||||
ResourceAccounts = "accounts"
|
||||
ResourceGPGKeys = "gpgkeys"
|
||||
|
||||
// please add new items to Actions
|
||||
ActionGet = "get"
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import (
|
|||
applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
|
||||
certificatepkg "github.com/argoproj/argo-cd/pkg/apiclient/certificate"
|
||||
clusterpkg "github.com/argoproj/argo-cd/pkg/apiclient/cluster"
|
||||
gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
repocredspkg "github.com/argoproj/argo-cd/pkg/apiclient/repocreds"
|
||||
repositorypkg "github.com/argoproj/argo-cd/pkg/apiclient/repository"
|
||||
|
|
@ -74,6 +75,7 @@ import (
|
|||
servercache "github.com/argoproj/argo-cd/server/cache"
|
||||
"github.com/argoproj/argo-cd/server/certificate"
|
||||
"github.com/argoproj/argo-cd/server/cluster"
|
||||
"github.com/argoproj/argo-cd/server/gpgkey"
|
||||
"github.com/argoproj/argo-cd/server/metrics"
|
||||
"github.com/argoproj/argo-cd/server/project"
|
||||
"github.com/argoproj/argo-cd/server/rbacpolicy"
|
||||
|
|
@ -463,6 +465,7 @@ func (a *ArgoCDServer) newGRPCServer() *grpc.Server {
|
|||
"/cluster.ClusterService/Update": true,
|
||||
"/session.SessionService/Create": true,
|
||||
"/account.AccountService/UpdatePassword": true,
|
||||
"/gpgkey.GPGKeyService/CreateGnuPGPublicKey": true,
|
||||
"/repository.RepositoryService/Create": true,
|
||||
"/repository.RepositoryService/Update": true,
|
||||
"/repository.RepositoryService/CreateRepository": true,
|
||||
|
|
@ -515,6 +518,7 @@ func (a *ArgoCDServer) newGRPCServer() *grpc.Server {
|
|||
settingsService := settings.NewServer(a.settingsMgr, a, a.DisableAuth)
|
||||
accountService := account.NewServer(a.sessionMgr, a.settingsMgr, a.enf)
|
||||
certificateService := certificate.NewServer(a.RepoClientset, db, a.enf)
|
||||
gpgkeyService := gpgkey.NewServer(a.RepoClientset, db, a.enf)
|
||||
versionpkg.RegisterVersionServiceServer(grpcS, &version.Server{})
|
||||
clusterpkg.RegisterClusterServiceServer(grpcS, clusterService)
|
||||
applicationpkg.RegisterApplicationServiceServer(grpcS, applicationService)
|
||||
|
|
@ -525,6 +529,7 @@ func (a *ArgoCDServer) newGRPCServer() *grpc.Server {
|
|||
projectpkg.RegisterProjectServiceServer(grpcS, projectService)
|
||||
accountpkg.RegisterAccountServiceServer(grpcS, accountService)
|
||||
certificatepkg.RegisterCertificateServiceServer(grpcS, certificateService)
|
||||
gpgkeypkg.RegisterGPGKeyServiceServer(grpcS, gpgkeyService)
|
||||
// Register reflection service on gRPC server.
|
||||
reflection.Register(grpcS)
|
||||
grpc_prometheus.Register(grpcS)
|
||||
|
|
@ -627,6 +632,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
|
|||
mustRegisterGWHandler(projectpkg.RegisterProjectServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
|
||||
mustRegisterGWHandler(accountpkg.RegisterAccountServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
|
||||
mustRegisterGWHandler(certificatepkg.RegisterCertificateServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
|
||||
mustRegisterGWHandler(gpgkeypkg.RegisterGPGKeyServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
|
||||
|
||||
// Swagger UI
|
||||
swagger.ServeSwaggerUI(mux, assets.SwaggerJSON, "/swagger-ui", a.RootPath)
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ repo-server: su --pty -m default -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=t
|
|||
ui: su --pty -m default -c "test \"$ARGOCD_IN_CI\" = \"true\" && exit 0; cd ui && ARGOCD_E2E_YARN_HOST=0.0.0.0 ${ARGOCD_E2E_YARN_CMD:-yarn} start"
|
||||
sshd: test "$ARGOCD_E2E_TEST" = "true" && /usr/sbin/sshd -p 2222 -D -e
|
||||
fcgiwrap: test "$ARGOCD_E2E_TEST" = "true" && (fcgiwrap -s unix:/var/run/fcgiwrap.socket & sleep 1 && chmod 777 /var/run/fcgiwrap.socket && wait)
|
||||
nginx: test "$ARGOCD_E2E_TEST" = "true" && nginx -g 'daemon off;' -c $(pwd)/test/fixture/testrepos/nginx.conf
|
||||
nginx: test "$ARGOCD_E2E_TEST" = "true" && nginx -g 'daemon off;' -c $(pwd)/test/fixture/testrepos/nginx.conf
|
||||
|
|
|
|||
|
|
@ -45,6 +45,52 @@ const (
|
|||
guestbookPathLocal = "./testdata/guestbook_local"
|
||||
)
|
||||
|
||||
func TestSyncToUnsignedCommit(t *testing.T) {
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
IgnoreErrors().
|
||||
Create().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing))
|
||||
}
|
||||
|
||||
func TestSyncToSignedCommitWithoutKnownKey(t *testing.T) {
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
Create().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
Expect(HealthIs(health.HealthStatusMissing))
|
||||
}
|
||||
|
||||
func TestSyncToSignedCommitKeyWithKnownKey(t *testing.T) {
|
||||
Given(t).
|
||||
Project("gpg").
|
||||
Path(guestbookPath).
|
||||
GPGPublicKeyAdded().
|
||||
Sleep(2).
|
||||
When().
|
||||
AddSignedFile("test.yaml", "null").
|
||||
IgnoreErrors().
|
||||
Create().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Expect(HealthIs(health.HealthStatusHealthy))
|
||||
}
|
||||
|
||||
func TestAppCreation(t *testing.T) {
|
||||
ctx := Given(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ func (a *Actions) AddFile(fileName, fileContents string) *Actions {
|
|||
return a
|
||||
}
|
||||
|
||||
func (a *Actions) AddSignedFile(fileName, fileContents string) *Actions {
|
||||
a.context.t.Helper()
|
||||
fixture.AddSignedFile(a.context.path+"/"+fileName, fileContents)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Actions) CreateFromFile(handler func(app *Application)) *Actions {
|
||||
a.context.t.Helper()
|
||||
app := &Application{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/test/e2e/fixture"
|
||||
"github.com/argoproj/argo-cd/test/e2e/fixture/certs"
|
||||
"github.com/argoproj/argo-cd/test/e2e/fixture/gpgkeys"
|
||||
"github.com/argoproj/argo-cd/test/e2e/fixture/repos"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
|
@ -42,6 +43,16 @@ func Given(t *testing.T) *Context {
|
|||
return &Context{t: t, destServer: KubernetesInternalAPIServerAddr, repoURLType: fixture.RepoURLTypeFile, name: fixture.Name(), timeout: 10, project: "default", prune: true}
|
||||
}
|
||||
|
||||
func (c *Context) GPGPublicKeyAdded() *Context {
|
||||
gpgkeys.AddGPGPublicKey()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) GPGPublicKeyRemoved() *Context {
|
||||
gpgkeys.DeleteGPGPublicKey()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) CustomCACertAdded() *Context {
|
||||
certs.AddCustomCACert()
|
||||
return c
|
||||
|
|
@ -217,6 +228,11 @@ func (c *Context) When() *Actions {
|
|||
return &Actions{context: c}
|
||||
}
|
||||
|
||||
func (c *Context) Sleep(seconds time.Duration) *Context {
|
||||
time.Sleep(seconds * time.Second)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) Prune(prune bool) *Context {
|
||||
c.prune = prune
|
||||
return c
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ const (
|
|||
RepoURLTypeHelm = "helm"
|
||||
GitUsername = "admin"
|
||||
GitPassword = "password"
|
||||
GpgGoodKeyID = "D56C4FCA57A46444"
|
||||
)
|
||||
|
||||
// getKubeConfig creates new kubernetes client config using specified config path and config overrides variables
|
||||
|
|
@ -319,6 +320,16 @@ func EnsureCleanState(t *testing.T) {
|
|||
ClusterResourceWhitelist: []v1.GroupKind{{Group: "*", Kind: "*"}},
|
||||
})
|
||||
|
||||
// Create seperate project for testing gpg signature verification
|
||||
FailOnErr(RunCli("proj", "create", "gpg"))
|
||||
SetProjectSpec("gpg", v1alpha1.AppProjectSpec{
|
||||
OrphanedResources: nil,
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []v1alpha1.ApplicationDestination{{Namespace: "*", Server: "*"}},
|
||||
ClusterResourceWhitelist: []v1.GroupKind{{Group: "*", Kind: "*"}},
|
||||
SignatureKeys: []v1alpha1.SignatureKey{{KeyID: GpgGoodKeyID}},
|
||||
})
|
||||
|
||||
// remove tmp dir
|
||||
CheckError(os.RemoveAll(TmpDir))
|
||||
|
||||
|
|
@ -335,6 +346,21 @@ func EnsureCleanState(t *testing.T) {
|
|||
FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/tls"))
|
||||
FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/ssh"))
|
||||
|
||||
// For signing during the tests
|
||||
FailOnErr(Run("", "mkdir", "-p", TmpDir+"/gpg"))
|
||||
FailOnErr(Run("", "chmod", "0700", TmpDir+"/gpg"))
|
||||
prevGnuPGHome := os.Getenv("GNUPGHOME")
|
||||
os.Setenv("GNUPGHOME", TmpDir+"/gpg")
|
||||
// nolint:errcheck
|
||||
Run("", "pkill", "-9", "gpg-agent")
|
||||
FailOnErr(Run("", "gpg", "--import", "../fixture/gpg/signingkey.asc"))
|
||||
os.Setenv("GNUPGHOME", prevGnuPGHome)
|
||||
|
||||
// recreate GPG directories
|
||||
FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/gpg/source"))
|
||||
FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/gpg/keys"))
|
||||
FailOnErr(Run("", "chmod", "0700", TmpDir+"/app/config/gpg/keys"))
|
||||
|
||||
// set-up tmp repo, must have unique name
|
||||
FailOnErr(Run("", "cp", "-Rf", "testdata", repoDirectory()))
|
||||
FailOnErr(Run(repoDirectory(), "chmod", "777", "."))
|
||||
|
|
@ -418,6 +444,18 @@ func AddFile(path, contents string) {
|
|||
FailOnErr(Run(repoDirectory(), "git", "commit", "-am", "add file"))
|
||||
}
|
||||
|
||||
func AddSignedFile(path, contents string) {
|
||||
log.WithFields(log.Fields{"path": path}).Info("adding")
|
||||
|
||||
CheckError(ioutil.WriteFile(filepath.Join(repoDirectory(), path), []byte(contents), 0644))
|
||||
prevGnuPGHome := os.Getenv("GNUPGHOME")
|
||||
os.Setenv("GNUPGHOME", TmpDir+"/gpg")
|
||||
FailOnErr(Run(repoDirectory(), "git", "diff"))
|
||||
FailOnErr(Run(repoDirectory(), "git", "add", "."))
|
||||
FailOnErr(Run(repoDirectory(), "git", "-c", fmt.Sprintf("user.signingkey=%s", GpgGoodKeyID), "commit", "-S", "-am", "add file"))
|
||||
os.Setenv("GNUPGHOME", prevGnuPGHome)
|
||||
}
|
||||
|
||||
// create the resource by creating using "kubectl apply", with bonus templating
|
||||
func Declarative(filename string, values interface{}) (string, error) {
|
||||
|
||||
|
|
|
|||
32
test/e2e/fixture/gpgkeys/gpgkeys.go
Normal file
32
test/e2e/fixture/gpgkeys/gpgkeys.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package gpgkeys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
|
||||
"github.com/argoproj/argo-cd/test/e2e/fixture"
|
||||
)
|
||||
|
||||
// Add GPG public key via API and create appropriate file where the ConfigMap mount would de it as well
|
||||
func AddGPGPublicKey() {
|
||||
keyPath, err := filepath.Abs(fmt.Sprintf("../fixture/gpg/%s", fixture.GpgGoodKeyID))
|
||||
errors.CheckError(err)
|
||||
args := []string{"gpg", "add", "--from", keyPath}
|
||||
errors.FailOnErr(fixture.RunCli(args...))
|
||||
|
||||
keyData, err := ioutil.ReadFile(keyPath)
|
||||
errors.CheckError(err)
|
||||
err = ioutil.WriteFile(fmt.Sprintf("%s/app/config/gpg/source/%s", fixture.TmpDir, fixture.GpgGoodKeyID), keyData, 0644)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
func DeleteGPGPublicKey() {
|
||||
args := []string{"gpg", "rm", fixture.GpgGoodKeyID}
|
||||
errors.FailOnErr(fixture.RunCli(args...))
|
||||
|
||||
errors.CheckError(os.Remove(fmt.Sprintf("%s/app/config/gpg/source/%s", fixture.TmpDir, fixture.GpgGoodKeyID)))
|
||||
}
|
||||
31
test/fixture/gpg/D56C4FCA57A46444
Normal file
31
test/fixture/gpg/D56C4FCA57A46444
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBF5vlTkBCADFX1JfbbIYZ+dv5QGRAZfiid8jAkcwJSRn0xwd0M3Dld2vDpKV
|
||||
UAfzul2sHFSpmwqkiXbXpl9QierO8fBBfmT5dZf63JdSzmiH5e8ysmSaC8iOAGBc
|
||||
nSX/35++8FxM1FtRXiK5/T6kfCQVRZhMKLnjIMREbvse+OMLouGq3JscywIdKvLO
|
||||
QDM/Ni1aH33grdQ+UbOFpT6Z1Jm0bGxrrzncwBWMLPePrYyVyletYTbXe9wbPx7e
|
||||
BnxsqVaeb3I6w4exWVjyGJKFMQWpcLOLZ0/7Osbs6Qk+BhI7IYgeVzG/0BeaKsZX
|
||||
vCTioKcpjAWEw7JRQWWmbl8dqtJr3tXUnt2NABEBAAG0PkFyZ29DRCBFMkUgVGVz
|
||||
dCBTaWduaW5nIEtleSAoRG8gbm90IHVzZSkgPG5vcmVwbHlAZXhhbXBsZS5jb20+
|
||||
iQFOBBMBCAA4FiEE6kWbSVlcvj/R+6MD1WxPylekZEQFAl5vlTkCGwMFCwkIBwIG
|
||||
FQoJCAsCBBYCAwECHgECF4AACgkQ1WxPylekZESZkAgAm8UZCNO1pKYRPT97agML
|
||||
vX9G8RJ9oBUG2rUZWcMJ09prpaHf1CpkipxBUDAG7ljjpPgl1Y4gmopEEUESvEgf
|
||||
g6PlNJfM83WE7gw548qOUTxlbCuLnKyLcUyr+YMo5DNgJG++VDG47MPP3fubO/ZL
|
||||
zSyACeek+bMYrYEy1e3O/bQ61AJIGJGEjxXhmNSLd8koLPFMrdupckshJ1T8tUGs
|
||||
P14qHUo4UCjytPCca8BN1KOiRcrykENte0S++Y3KT6Z+qMrrh0gb6JYU76oD8AU2
|
||||
tz9S/lb9Yeg/mpefnPB7bEVKslvZdSPrBWGu6vD16MQ3KQRgvmkVp+L22PGZ1N46
|
||||
WrkBDQReb5U5AQgAukdIaK1cDe1Q2QXS3e2NLiCNtQgEvJGru2o7Nz58NkWv2vMW
|
||||
a7Q+JLdFltvciFHaq3HRaw3Xr4ejhKYFYuaHbKBtVu/CbQSP/e6nm2zyfqU6wHgS
|
||||
nGnFg9toQAcOEE0Sdz5J4plSL4osMJ6LVo3DHqf3wkYX2ajK+cAvKRTj/O9oF1PV
|
||||
nBUEYOu5GOP0dczpNz6TP5QBMjwU6ORcxxkqX/cY8io/sdaC77PR1Xrmul92NJ4B
|
||||
kM7fFBd3QCSjhxADYnWGgWogQ6B865V8lpNX4GI6tXVRLjF/XjuapoSiPmax+Al4
|
||||
Wt2W5m/K4+/Fk0nO0SZawcpUrGI82yTp3CmGXwARAQABiQE2BBgBCAAgFiEE6kWb
|
||||
SVlcvj/R+6MD1WxPylekZEQFAl5vlTkCGwwACgkQ1WxPylekZETt9gf+JNcc003S
|
||||
BEyfr/WYz65ktu5kLoGPtjUF1dBF/6MCFS5SCp/rEaK0y8R09I8wYb90sRVB80lM
|
||||
vES/Ec0KD0beE1vjVAbURIrGC9fXK7lFo+KoBHmdDSKdkP8t289CrZ9g2n4orr2M
|
||||
aLVobOh8Q8eXR6xyguDR8OcgSbUaHuY8ZsEyeS9IH+p97GojD/mwnZletu4wleDT
|
||||
2DdUIsiV6L1d40mxRGdCpNAzPzibFn1Nh6dr5yzH0ihaTQ4He4MqcZZgYJXODZFV
|
||||
5shN+mHofvXnJ5Wt4sKzRH+A6vg/jMUxXoSU3Gu0a/RGUn8Cz6cX9boe6M1Fi+xd
|
||||
LZtzetfNy78xfg==
|
||||
=TpiH
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
BIN
test/fixture/gpg/signingkey.asc
Normal file
BIN
test/fixture/gpg/signingkey.asc
Normal file
Binary file not shown.
|
|
@ -3,6 +3,7 @@ package test
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
|
@ -59,3 +60,12 @@ func portIsOpen(addr string) bool {
|
|||
_ = conn.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// Read the contents of a file and returns it as string. Panics on error.
|
||||
func MustLoadFileToString(path string) string {
|
||||
o, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return string(o)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ export const RevisionMetadataRows = (props: {applicationName: string; source: Ap
|
|||
<div className='columns small-9'>{m.message}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='row'>
|
||||
<div className='columns small-3'>GPG signature</div>
|
||||
<div className='columns small-9'>{m.signatureInfo || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DataLoader>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export const RevisionMetadataPanel = (props: {appName: string; type: string; rev
|
|||
<br />
|
||||
</span>
|
||||
)}
|
||||
{m.signatureInfo}
|
||||
<br />
|
||||
{m.message}
|
||||
</span>
|
||||
}
|
||||
|
|
@ -31,7 +33,7 @@ export const RevisionMetadataPanel = (props: {appName: string; type: string; rev
|
|||
<div className='application-status-panel__item-name'>
|
||||
{m.author && (
|
||||
<React.Fragment>
|
||||
Authored by {m.author}
|
||||
Authored by {m.author} - {m.signatureInfo}
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
@import 'node_modules/argo-ui/src/styles/config';
|
||||
|
||||
.gpgkeys-list {
|
||||
&__top-panel {
|
||||
padding: 1em;
|
||||
|
||||
& > .columns:first-child {
|
||||
font-size: 8em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& > .columns:last-child {
|
||||
text-align: center;
|
||||
border-left: 2px solid $argo-color-gray-4;
|
||||
|
||||
& > p {
|
||||
margin-bottom: 0;
|
||||
margin-top: 24px;
|
||||
&:first-of-type {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
}
|
||||
|
||||
& > button {
|
||||
width: 15em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.argo-table-list {
|
||||
.argo-dropdown {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
textarea.argo-field {
|
||||
height: 25em;
|
||||
width: 1024em;
|
||||
white-space: pre;
|
||||
overflow-wrap: normal;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
196
ui/src/app/settings/components/gpgkeys-list/gpgkeys-list.tsx
Normal file
196
ui/src/app/settings/components/gpgkeys-list/gpgkeys-list.tsx
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import {DropDownMenu, FormField, NotificationType, SlidingPanel} from 'argo-ui';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import {Form, FormApi, TextArea} from 'react-form';
|
||||
import {RouteComponentProps} from 'react-router';
|
||||
|
||||
import {DataLoader, EmptyState, ErrorNotification, Page} from '../../../shared/components';
|
||||
import {AppContext} from '../../../shared/context';
|
||||
import * as models from '../../../shared/models';
|
||||
import {services} from '../../../shared/services';
|
||||
|
||||
require('./gpgkeys-list.scss');
|
||||
|
||||
interface NewGnuPGPublicKeyParams {
|
||||
keyData: string;
|
||||
}
|
||||
|
||||
export class GpgKeysList extends React.Component<RouteComponentProps<any>> {
|
||||
public static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
apis: PropTypes.object,
|
||||
history: PropTypes.object
|
||||
};
|
||||
|
||||
private formApi: FormApi;
|
||||
private loader: DataLoader;
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Page
|
||||
title='GnuPG public keys'
|
||||
toolbar={{
|
||||
breadcrumbs: [{title: 'Settings', path: '/settings'}, {title: 'GnuPG public keys'}],
|
||||
actionMenu: {
|
||||
className: 'fa fa-plus',
|
||||
items: [
|
||||
{
|
||||
title: 'Add GnuPG key',
|
||||
action: () => (this.showAddGnuPGKey = true)
|
||||
}
|
||||
]
|
||||
}
|
||||
}}>
|
||||
<div className='gpgkeys-list'>
|
||||
<div className='argo-container'>
|
||||
<DataLoader load={() => services.gpgkeys.list()} ref={loader => (this.loader = loader)}>
|
||||
{(gpgkeys: models.GnuPGPublicKey[]) =>
|
||||
(gpgkeys.length > 0 && (
|
||||
<div className='argo-table-list'>
|
||||
<div className='argo-table-list__head'>
|
||||
<div className='row'>
|
||||
<div className='columns small-3'>KEY ID</div>
|
||||
<div className='columns small-3'>KEY TYPE</div>
|
||||
<div className='columns small-6'>IDENTITY</div>
|
||||
</div>
|
||||
</div>
|
||||
{gpgkeys.map(gpgkey => (
|
||||
<div className='argo-table-list__row' key={gpgkey.keyID}>
|
||||
<div className='row'>
|
||||
<div className='columns small-3'>
|
||||
<i className='fa fa-key' /> {gpgkey.keyID}
|
||||
</div>
|
||||
<div className='columns small-3'>{gpgkey.subType.toUpperCase()}</div>
|
||||
<div className='columns small-6'>
|
||||
{gpgkey.owner}
|
||||
<DropDownMenu
|
||||
anchor={() => (
|
||||
<button className='argo-button argo-button--light argo-button--lg argo-button--short'>
|
||||
<i className='fa fa-ellipsis-v' />
|
||||
</button>
|
||||
)}
|
||||
items={[
|
||||
{
|
||||
title: 'Remove',
|
||||
action: () => this.removeKey(gpgkey.keyID)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)) || (
|
||||
<EmptyState icon='fa fa-key'>
|
||||
<h4>No GnuPG public keys currently configured</h4>
|
||||
<h5>You can add GnuPG public keys below..</h5>
|
||||
<button className='argo-button argo-button--base' onClick={() => (this.showAddGnuPGKey = true)}>
|
||||
Add GnuPG public key
|
||||
</button>
|
||||
</EmptyState>
|
||||
)
|
||||
}
|
||||
</DataLoader>
|
||||
</div>
|
||||
</div>
|
||||
<SlidingPanel
|
||||
isShown={this.showAddGnuPGKey}
|
||||
onClose={() => (this.showAddGnuPGKey = false)}
|
||||
header={
|
||||
<div>
|
||||
<button className='argo-button argo-button--base' onClick={() => this.formApi.submitForm(null)}>
|
||||
Create
|
||||
</button>{' '}
|
||||
<button onClick={() => (this.showAddGnuPGKey = false)} className='argo-button argo-button--base-o'>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
}>
|
||||
<h4>Add GnuPG public key</h4>
|
||||
<Form
|
||||
onSubmit={params => this.addGnuPGPublicKey({keyData: params.keyData})}
|
||||
getApi={api => (this.formApi = api)}
|
||||
preSubmit={(params: NewGnuPGPublicKeyParams) => ({
|
||||
keyData: params.keyData
|
||||
})}
|
||||
validateError={(params: NewGnuPGPublicKeyParams) => ({
|
||||
keyData: !params.keyData && 'Key data is required'
|
||||
})}>
|
||||
{formApi => (
|
||||
<form onSubmit={formApi.submitForm} role='form' className='gpgkeys-list width-control' encType='multipart/form-data'>
|
||||
<div className='argo-form-row'>
|
||||
<FormField formApi={formApi} label='GnuPG public key data (ASCII-armored)' field='keyData' component={TextArea} />
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</SlidingPanel>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
private clearForms() {
|
||||
this.formApi.resetAll();
|
||||
}
|
||||
|
||||
private validateKeyInputfield(data: string): boolean {
|
||||
if (data == null || data === '') {
|
||||
return false;
|
||||
}
|
||||
const str = data.trim();
|
||||
const startNeedle = '-----BEGIN PGP PUBLIC KEY BLOCK-----\n';
|
||||
const endNeedle = '\n-----END PGP PUBLIC KEY BLOCK-----';
|
||||
|
||||
if (str.length < startNeedle.length + endNeedle.length) {
|
||||
return false;
|
||||
}
|
||||
if (!str.startsWith(startNeedle)) {
|
||||
return false;
|
||||
}
|
||||
if (!str.endsWith(endNeedle)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async addGnuPGPublicKey(params: NewGnuPGPublicKeyParams) {
|
||||
try {
|
||||
if (!this.validateKeyInputfield(params.keyData)) {
|
||||
throw {
|
||||
name: 'Invalid key exception',
|
||||
message: 'Invalid GnuPG key data found - must be ASCII armored'
|
||||
};
|
||||
}
|
||||
await services.gpgkeys.create({keyData: params.keyData});
|
||||
this.showAddGnuPGKey = false;
|
||||
this.loader.reload();
|
||||
} catch (e) {
|
||||
this.appContext.apis.notifications.show({
|
||||
content: <ErrorNotification title='Unable to add GnuPG public key' e={e} />,
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async removeKey(keyId: string) {
|
||||
const confirmed = await this.appContext.apis.popup.confirm('Remove GPG public key', 'Are you sure you want to remove GPG key with ID ' + keyId + '?');
|
||||
if (confirmed) {
|
||||
await services.gpgkeys.delete(keyId);
|
||||
this.loader.reload();
|
||||
}
|
||||
}
|
||||
|
||||
private get showAddGnuPGKey() {
|
||||
return new URLSearchParams(this.props.location.search).get('addGnuPGPublicKey') === 'true';
|
||||
}
|
||||
|
||||
private set showAddGnuPGKey(val: boolean) {
|
||||
this.clearForms();
|
||||
this.appContext.router.history.push(`${this.props.match.url}?addGnuPGPublicKey=${val}`);
|
||||
}
|
||||
|
||||
private get appContext(): AppContext {
|
||||
return this.context as AppContext;
|
||||
}
|
||||
}
|
||||
|
|
@ -136,6 +136,7 @@ export class ProjectDetails extends React.Component<RouteComponentProps<{name: s
|
|||
namespaceResourceWhitelist: proj.spec.namespaceResourceWhitelist || [],
|
||||
roles: proj.spec.roles || [],
|
||||
syncWindows: proj.spec.syncWindows || [],
|
||||
signatureKeys: proj.spec.signatureKeys || [],
|
||||
orphanedResourcesEnabled: !!proj.spec.orphanedResources,
|
||||
orphanedResourcesWarn:
|
||||
proj.spec.orphanedResources && (proj.spec.orphanedResources.warn === undefined || proj.spec.orphanedResources.warn)
|
||||
|
|
@ -598,6 +599,28 @@ export class ProjectDetails extends React.Component<RouteComponentProps<{name: s
|
|||
</div>
|
||||
)}
|
||||
|
||||
<h4>Required signature keys {helpTip('IDs of GnuPG keys that commits must be signed with in order to be allowed to sync to')}</h4>
|
||||
{((proj.spec.signatureKeys || []).length > 0 && (
|
||||
<div className='argo-table-list'>
|
||||
<div className='argo-table-list__head'>
|
||||
<div className='row'>
|
||||
<div className='columns small-9'>KEY ID</div>
|
||||
</div>
|
||||
</div>
|
||||
{(proj.spec.signatureKeys || []).map(res => (
|
||||
<div className='argo-table-list__row' key={`${res.keyID}`}>
|
||||
<div className='row'>
|
||||
<div className='columns small-9'>{res.keyID}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)) || (
|
||||
<div className='white-box'>
|
||||
<p>Commit signatures are not required</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h4>Orphaned Resource Monitoring {helpTip('Enables monitoring of top level resources in the application target namespace')}</h4>
|
||||
|
||||
<div className='white-box'>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const ProjectEditPanel = (props: {nameReadonly?: boolean; defaultParams?:
|
|||
clusterResourceWhitelist: [],
|
||||
namespaceResourceBlacklist: [],
|
||||
namespaceResourceWhitelist: [],
|
||||
signatureKeys: [],
|
||||
...props.defaultParams
|
||||
}}
|
||||
validateError={(params: ProjectParams) => ({
|
||||
|
|
@ -200,6 +201,31 @@ export const ProjectEditPanel = (props: {nameReadonly?: boolean; defaultParams?:
|
|||
</a>
|
||||
</React.Fragment>
|
||||
|
||||
<DataLoader load={() => services.gpgkeys.list().then(gpgkeys => gpgkeys.map(gpgkey => gpgkey.keyID))}>
|
||||
{gpgkeys => (
|
||||
<React.Fragment>
|
||||
<h4>Required signature keys</h4>
|
||||
<div>GnuPG key IDs which commits to be synced to must be signed with</div>
|
||||
{(api.values.signatureKeys as Array<string>).map((_, i) => (
|
||||
<div key={i} className='row project-edit-panel__form-row'>
|
||||
<div className='columns small-12'>
|
||||
<FormField
|
||||
formApi={api}
|
||||
field={`signatureKeys[${i}].keyID`}
|
||||
component={AutocompleteField}
|
||||
componentProps={{
|
||||
items: gpgkeys
|
||||
}}
|
||||
/>
|
||||
<i className='fa fa-times' onClick={() => api.setValue('signatureKeys', removeEl(api.values.signatureKeys, i))} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<a onClick={() => api.setValue('signatureKeys', api.values.signatureKeys.concat(gpgkeys[0]))}>add GnuPG key ID</a>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</DataLoader>
|
||||
|
||||
<React.Fragment>
|
||||
<h4>Orphaned Resource Monitoring</h4>
|
||||
<div>Enables monitoring of top level resources in the application target namespace</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {AccountDetails} from './account-details/account-details';
|
|||
import {AccountsList} from './accounts-list/accounts-list';
|
||||
import {CertsList} from './certs-list/certs-list';
|
||||
import {ClustersList} from './clusters-list/clusters-list';
|
||||
import {GpgKeysList} from './gpgkeys-list/gpgkeys-list';
|
||||
import {ProjectDetails} from './project-details/project-details';
|
||||
import {ProjectsList} from './projects-list/projects-list';
|
||||
import {ReposList} from './repos-list/repos-list';
|
||||
|
|
@ -15,6 +16,7 @@ export const SettingsContainer = (props: RouteComponentProps<any>) => (
|
|||
<Route exact={true} path={`${props.match.path}`} component={SettingsOverview} />
|
||||
<Route exact={true} path={`${props.match.path}/repos`} component={ReposList} />
|
||||
<Route exact={true} path={`${props.match.path}/certs`} component={CertsList} />
|
||||
<Route exact={true} path={`${props.match.path}/gpgkeys`} component={GpgKeysList} />
|
||||
<Route exact={true} path={`${props.match.path}/clusters`} component={ClustersList} />
|
||||
<Route exact={true} path={`${props.match.path}/projects`} component={ProjectsList} />
|
||||
<Route exact={true} path={`${props.match.path}/projects/:name`} component={ProjectDetails} />
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ const settings = [
|
|||
description: 'Configure certificates for connecting Git repositories',
|
||||
path: './certs'
|
||||
},
|
||||
{
|
||||
title: 'GnuPG keys',
|
||||
description: 'Configure GnuPG public keys for commit verification',
|
||||
path: './gpgkeys'
|
||||
},
|
||||
{
|
||||
title: 'Clusters',
|
||||
description: 'Configure connected Kubernetes clusters',
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export interface RevisionMetadata {
|
|||
date: models.Time;
|
||||
tags?: string[];
|
||||
message?: string;
|
||||
signatureInfo?: string;
|
||||
}
|
||||
|
||||
export interface SyncOperationResult {
|
||||
|
|
@ -587,6 +588,10 @@ export interface GroupKind {
|
|||
kind: string;
|
||||
}
|
||||
|
||||
export interface ProjectSignatureKey {
|
||||
keyID: string;
|
||||
}
|
||||
|
||||
export interface ProjectSpec {
|
||||
sourceRepos: string[];
|
||||
destinations: ApplicationDestination[];
|
||||
|
|
@ -595,6 +600,7 @@ export interface ProjectSpec {
|
|||
clusterResourceWhitelist: GroupKind[];
|
||||
namespaceResourceBlacklist: GroupKind[];
|
||||
namespaceResourceWhitelist: GroupKind[];
|
||||
signatureKeys: ProjectSignatureKey[];
|
||||
orphanedResources?: {warn?: boolean};
|
||||
syncWindows?: SyncWindows;
|
||||
}
|
||||
|
|
@ -668,3 +674,13 @@ export interface Account {
|
|||
capabilities: string[];
|
||||
tokens: Token[];
|
||||
}
|
||||
|
||||
export interface GnuPGPublicKey {
|
||||
keyID?: string;
|
||||
fingerprint?: string;
|
||||
subType?: string;
|
||||
owner?: string;
|
||||
keyData?: string;
|
||||
}
|
||||
|
||||
export interface GnuPGPublicKeyList extends ItemsList<GnuPGPublicKey> {}
|
||||
|
|
|
|||
26
ui/src/app/shared/services/gpgkey-service.ts
Normal file
26
ui/src/app/shared/services/gpgkey-service.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import * as models from '../models';
|
||||
import requests from './requests';
|
||||
|
||||
export class GnuPGPublicKeyService {
|
||||
public list(): Promise<models.GnuPGPublicKey[]> {
|
||||
return requests
|
||||
.get('/gpgkeys')
|
||||
.then(res => res.body as models.GnuPGPublicKeyList)
|
||||
.then(list => list.items || []);
|
||||
}
|
||||
|
||||
public create(publickey: models.GnuPGPublicKey): Promise<models.GnuPGPublicKeyList> {
|
||||
return requests
|
||||
.post('/gpgkeys')
|
||||
.send(publickey)
|
||||
.then(res => res.body as models.GnuPGPublicKeyList);
|
||||
}
|
||||
|
||||
public delete(keyID: string): Promise<models.GnuPGPublicKey> {
|
||||
return requests
|
||||
.delete('/gpgkeys')
|
||||
.query({keyID})
|
||||
.send()
|
||||
.then(res => res.body as models.GnuPGPublicKey);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import {ApplicationsService} from './applications-service';
|
|||
import {AuthService} from './auth-service';
|
||||
import {CertificatesService} from './cert-service';
|
||||
import {ClustersService} from './clusters-service';
|
||||
import {GnuPGPublicKeyService} from './gpgkey-service';
|
||||
import {ProjectsService} from './projects-service';
|
||||
import {RepositoriesService} from './repo-service';
|
||||
import {RepoCredsService} from './repocreds-service';
|
||||
|
|
@ -22,6 +23,7 @@ export interface Services {
|
|||
viewPreferences: ViewPreferencesService;
|
||||
version: VersionService;
|
||||
accounts: AccountsService;
|
||||
gpgkeys: GnuPGPublicKeyService;
|
||||
}
|
||||
|
||||
export const services: Services = {
|
||||
|
|
@ -35,7 +37,8 @@ export const services: Services = {
|
|||
projects: new ProjectsService(),
|
||||
viewPreferences: new ViewPreferencesService(),
|
||||
version: new VersionService(),
|
||||
accounts: new AccountsService()
|
||||
accounts: new AccountsService(),
|
||||
gpgkeys: new GnuPGPublicKeyService()
|
||||
};
|
||||
|
||||
export {ProjectParams, ProjectRoleParams, CreateJWTTokenParams, DeleteJWTTokenParams, JWTTokenResponse} from './projects-service';
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface ProjectParams {
|
|||
clusterResourceWhitelist: models.GroupKind[];
|
||||
namespaceResourceBlacklist: models.GroupKind[];
|
||||
namespaceResourceWhitelist: models.GroupKind[];
|
||||
signatureKeys: models.ProjectSignatureKey[];
|
||||
orphanedResourcesEnabled: boolean;
|
||||
orphanedResourcesWarn: boolean;
|
||||
syncWindows: models.SyncWindow[];
|
||||
|
|
@ -80,6 +81,7 @@ function paramsToProj(params: ProjectParams) {
|
|||
clusterResourceWhitelist: params.clusterResourceWhitelist,
|
||||
namespaceResourceBlacklist: params.namespaceResourceBlacklist,
|
||||
namespaceResourceWhitelist: params.namespaceResourceWhitelist,
|
||||
signatureKeys: params.signatureKeys,
|
||||
orphanedResources: (params.orphanedResourcesEnabled && {warn: !!params.orphanedResourcesWarn}) || null
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -59,6 +59,13 @@ type ArgoDB interface {
|
|||
|
||||
// ListHelmRepositories lists repositories
|
||||
ListHelmRepositories(ctx context.Context) ([]*appv1.Repository, error)
|
||||
|
||||
// ListConfiguredGPGPublicKeys returns all GPG public key IDs that are configured
|
||||
ListConfiguredGPGPublicKeys(ctx context.Context) (map[string]*appv1.GnuPGPublicKey, error)
|
||||
// AddGPGPublicKey adds one ore more GPG public keys to the configuration
|
||||
AddGPGPublicKey(ctx context.Context, keyData string) (map[string]*appv1.GnuPGPublicKey, []string, error)
|
||||
// DeleteGPGPublicKey removes a GPG public key from the configuration
|
||||
DeleteGPGPublicKey(ctx context.Context, keyID string) error
|
||||
}
|
||||
|
||||
type db struct {
|
||||
|
|
|
|||
142
util/db/gpgkeys.go
Normal file
142
util/db/gpgkeys.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
)
|
||||
|
||||
// Validates a single GnuPG key and returns the key's ID
|
||||
func validatePGPKey(keyData string) (*appsv1.GnuPGPublicKey, error) {
|
||||
f, err := ioutil.TempFile("", "gpg-public-key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
err = ioutil.WriteFile(f.Name(), []byte(keyData), 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Close()
|
||||
|
||||
parsed, err := gpg.ValidatePGPKeys(f.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Each key/value pair in the config map must exactly contain one public key, with the (short) GPG key ID as key
|
||||
if len(parsed) != 1 {
|
||||
return nil, fmt.Errorf("More than one key found in input data")
|
||||
}
|
||||
|
||||
var retKey *appsv1.GnuPGPublicKey = nil
|
||||
// Is there a better way to get the first element from a map without knowing its key?
|
||||
for _, k := range parsed {
|
||||
retKey = k
|
||||
break
|
||||
}
|
||||
if retKey != nil {
|
||||
retKey.KeyData = keyData
|
||||
return retKey, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("Could not find the GPG key")
|
||||
}
|
||||
}
|
||||
|
||||
// ListConfiguredGPGPublicKeys returns a list of all configured GPG public keys from the ConfigMap
|
||||
func (db *db) ListConfiguredGPGPublicKeys(ctx context.Context) (map[string]*appsv1.GnuPGPublicKey, error) {
|
||||
log.Debugf("Loading PGP public keys from config map")
|
||||
result := make(map[string]*appsv1.GnuPGPublicKey)
|
||||
keysCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We have to verify all PGP keys in the ConfigMap to be valid keys before. To do so,
|
||||
// we write each single one out to a temporary file and validate them through gpg.
|
||||
// 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 {
|
||||
if expectedKeyID := gpg.KeyID(k); expectedKeyID != "" {
|
||||
parsedKey, err := validatePGPKey(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not parse GPG key for entry '%s': %s", expectedKeyID, err.Error())
|
||||
}
|
||||
if expectedKeyID != parsedKey.KeyID {
|
||||
return nil, fmt.Errorf("Key parsed for entry with key ID '%s' had different key ID '%s'", expectedKeyID, parsedKey.KeyID)
|
||||
}
|
||||
result[parsedKey.KeyID] = parsedKey
|
||||
} else {
|
||||
return nil, fmt.Errorf("Found entry with key '%s' in ConfigMap, but this is not a valid PGP key ID", k)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AddGPGPublicKey adds one or more public keys to the configuration
|
||||
func (db *db) AddGPGPublicKey(ctx context.Context, keyData string) (map[string]*appsv1.GnuPGPublicKey, []string, error) {
|
||||
result := make(map[string]*appsv1.GnuPGPublicKey)
|
||||
skipped := make([]string, 0)
|
||||
|
||||
keys, err := gpg.ValidatePGPKeysFromString(keyData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
keysCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if keysCM.Data == nil {
|
||||
keysCM.Data = make(map[string]string)
|
||||
}
|
||||
|
||||
for kid, key := range keys {
|
||||
if _, ok := keysCM.Data[kid]; ok {
|
||||
skipped = append(skipped, kid)
|
||||
log.Debugf("Not adding incoming key with kid=%s because it is configured already", kid)
|
||||
} else {
|
||||
result[kid] = key
|
||||
keysCM.Data[kid] = key.KeyData
|
||||
log.Debugf("Adding incoming key with kid=%s to database", kid)
|
||||
}
|
||||
}
|
||||
|
||||
err = db.settingsMgr.SaveGPGPublicKeyData(ctx, keysCM.Data)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return result, skipped, nil
|
||||
}
|
||||
|
||||
// DeleteGPGPublicKey deletes a GPG public key from the configuration
|
||||
func (db *db) DeleteGPGPublicKey(ctx context.Context, keyID string) error {
|
||||
keysCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if keysCM.Data == nil {
|
||||
return fmt.Errorf("No such key configured: %s", keyID)
|
||||
}
|
||||
|
||||
if _, ok := keysCM.Data[keyID]; !ok {
|
||||
return fmt.Errorf("No such key configured: %s", keyID)
|
||||
}
|
||||
|
||||
delete(keysCM.Data, keyID)
|
||||
|
||||
err = db.settingsMgr.SaveGPGPublicKeyData(ctx, keysCM.Data)
|
||||
return err
|
||||
}
|
||||
304
util/db/gpgkeys_test.go
Normal file
304
util/db/gpgkeys_test.go
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
// GPG config map with a single key and good mapping
|
||||
var gpgCMEmpty = v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDGPGKeysConfigMapName,
|
||||
Namespace: testNamespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// GPG config map with a single key and good mapping
|
||||
var gpgCMSingleGoodPubkey = v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDGPGKeysConfigMapName,
|
||||
Namespace: testNamespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
"4AEE18F83AFDEB23": test.MustLoadFileToString("../gpg/testdata/github.asc"),
|
||||
},
|
||||
}
|
||||
|
||||
// GPG config map with two keys and good mapping
|
||||
var gpgCMMultiGoodPubkey = v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDGPGKeysConfigMapName,
|
||||
Namespace: testNamespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
"FDC79815400D88A9": test.MustLoadFileToString("../gpg/testdata/johndoe.asc"),
|
||||
"F7842A5CEAA9C0B1": test.MustLoadFileToString("../gpg/testdata/janedoe.asc"),
|
||||
},
|
||||
}
|
||||
|
||||
// GPG config map with a single key and bad mapping
|
||||
var gpgCMSingleKeyWrongId = v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDGPGKeysConfigMapName,
|
||||
Namespace: testNamespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
"5AEE18F83AFDEB23": test.MustLoadFileToString("../gpg/testdata/github.asc"),
|
||||
},
|
||||
}
|
||||
|
||||
// GPG config map with a garbage pub key
|
||||
var gpgCMGarbagePubkey = v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDGPGKeysConfigMapName,
|
||||
Namespace: testNamespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
"4AEE18F83AFDEB23": test.MustLoadFileToString("../gpg/testdata/garbage.asc"),
|
||||
},
|
||||
}
|
||||
|
||||
// GPG config map with a wrong key
|
||||
var gpgCMGarbageCMKey = v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDGPGKeysConfigMapName,
|
||||
Namespace: testNamespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
"wullerosekaufe": test.MustLoadFileToString("../gpg/testdata/github.asc"),
|
||||
},
|
||||
}
|
||||
|
||||
// Returns a fake client set for use in tests
|
||||
func getGPGKeysClientset(gpgCM v1.ConfigMap) *fake.Clientset {
|
||||
cm := v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-cm",
|
||||
Namespace: testNamespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
return fake.NewSimpleClientset([]runtime.Object{&cm, &gpgCM}...)
|
||||
}
|
||||
|
||||
func Test_ValidatePGPKey(t *testing.T) {
|
||||
// Good case - single PGP key
|
||||
{
|
||||
key, err := validatePGPKey(test.MustLoadFileToString("../gpg/testdata/github.asc"))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, key)
|
||||
assert.Equal(t, "4AEE18F83AFDEB23", key.KeyID)
|
||||
assert.NotEmpty(t, key.Owner)
|
||||
assert.NotEmpty(t, key.KeyData)
|
||||
assert.NotEmpty(t, key.SubType)
|
||||
}
|
||||
// Bad case - Garbage
|
||||
{
|
||||
key, err := validatePGPKey(test.MustLoadFileToString("../gpg/testdata/garbage.asc"))
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, key)
|
||||
}
|
||||
// Bad case - more than one key
|
||||
{
|
||||
key, err := validatePGPKey(test.MustLoadFileToString("../gpg/testdata/multi.asc"))
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, key)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ListConfiguredGPGPublicKeys(t *testing.T) {
|
||||
// Good case. Single key in input, right mapping to Key ID in CM
|
||||
{
|
||||
clientset := getGPGKeysClientset(gpgCMSingleGoodPubkey)
|
||||
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
|
||||
db := NewDB(testNamespace, settings, clientset)
|
||||
if db == nil {
|
||||
panic("could not get database")
|
||||
}
|
||||
keys, err := db.ListConfiguredGPGPublicKeys(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
}
|
||||
// Good case. No certificates in ConfigMap
|
||||
{
|
||||
clientset := getGPGKeysClientset(gpgCMEmpty)
|
||||
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
|
||||
db := NewDB(testNamespace, settings, clientset)
|
||||
if db == nil {
|
||||
panic("could not get database")
|
||||
}
|
||||
keys, err := db.ListConfiguredGPGPublicKeys(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 0)
|
||||
}
|
||||
// Bad case. Single key in input, wrong mapping to Key ID in CM
|
||||
{
|
||||
clientset := getGPGKeysClientset(gpgCMSingleKeyWrongId)
|
||||
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
|
||||
db := NewDB(testNamespace, settings, clientset)
|
||||
if db == nil {
|
||||
panic("could not get database")
|
||||
}
|
||||
keys, err := db.ListConfiguredGPGPublicKeys(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, keys, 0)
|
||||
}
|
||||
// Bad case. Garbage public key
|
||||
{
|
||||
clientset := getGPGKeysClientset(gpgCMGarbagePubkey)
|
||||
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
|
||||
db := NewDB(testNamespace, settings, clientset)
|
||||
if db == nil {
|
||||
panic("could not get database")
|
||||
}
|
||||
keys, err := db.ListConfiguredGPGPublicKeys(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, keys, 0)
|
||||
}
|
||||
// Bad case. Garbage ConfigMap key in data
|
||||
{
|
||||
clientset := getGPGKeysClientset(gpgCMGarbageCMKey)
|
||||
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
|
||||
db := NewDB(testNamespace, settings, clientset)
|
||||
if db == nil {
|
||||
panic("could not get database")
|
||||
}
|
||||
keys, err := db.ListConfiguredGPGPublicKeys(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, keys, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_AddGPGPublicKey(t *testing.T) {
|
||||
// Good case
|
||||
{
|
||||
clientset := getGPGKeysClientset(gpgCMEmpty)
|
||||
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
|
||||
db := NewDB(testNamespace, settings, clientset)
|
||||
|
||||
// Key should be added
|
||||
new, skipped, err := db.AddGPGPublicKey(context.Background(), test.MustLoadFileToString("../gpg/testdata/github.asc"))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, new, 1)
|
||||
assert.Len(t, skipped, 0)
|
||||
cm, err := settings.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, cm.Data, 1)
|
||||
|
||||
// Same key should not be added, but skipped
|
||||
new, skipped, err = db.AddGPGPublicKey(context.Background(), test.MustLoadFileToString("../gpg/testdata/github.asc"))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, new, 0)
|
||||
assert.Len(t, skipped, 1)
|
||||
cm, err = settings.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, cm.Data, 1)
|
||||
|
||||
// New keys should be added
|
||||
new, skipped, err = db.AddGPGPublicKey(context.Background(), test.MustLoadFileToString("../gpg/testdata/multi.asc"))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, new, 2)
|
||||
assert.Len(t, skipped, 0)
|
||||
cm, err = settings.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, cm.Data, 3)
|
||||
|
||||
// Same new keys should be skipped
|
||||
new, skipped, err = db.AddGPGPublicKey(context.Background(), test.MustLoadFileToString("../gpg/testdata/multi.asc"))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, new, 0)
|
||||
assert.Len(t, skipped, 2)
|
||||
cm, err = settings.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, cm.Data, 3)
|
||||
|
||||
// Garbage input should result in error
|
||||
new, skipped, err = db.AddGPGPublicKey(context.Background(), test.MustLoadFileToString("../gpg/testdata/garbage.asc"))
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, new)
|
||||
assert.Nil(t, skipped)
|
||||
cm, err = settings.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, cm.Data, 3)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DeleteGPGPublicKey(t *testing.T) {
|
||||
defer os.Setenv("GNUPGHOME", "")
|
||||
// Good case
|
||||
{
|
||||
clientset := getGPGKeysClientset(gpgCMMultiGoodPubkey)
|
||||
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
|
||||
db := NewDB(testNamespace, settings, clientset)
|
||||
|
||||
// Key should be removed
|
||||
err := db.DeleteGPGPublicKey(context.Background(), "FDC79815400D88A9")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Key should not exist anymore, therefore can't be deleted again
|
||||
err = db.DeleteGPGPublicKey(context.Background(), "FDC79815400D88A9")
|
||||
assert.Error(t, err)
|
||||
|
||||
// One key left in configuration
|
||||
n, err := db.ListConfiguredGPGPublicKeys(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, n, 1)
|
||||
|
||||
// Key should be removed
|
||||
err = db.DeleteGPGPublicKey(context.Background(), "F7842A5CEAA9C0B1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Key should not exist anymore, therefore can't be deleted again
|
||||
err = db.DeleteGPGPublicKey(context.Background(), "F7842A5CEAA9C0B1")
|
||||
assert.Error(t, err)
|
||||
|
||||
// No key left in configuration
|
||||
n, err = db.ListConfiguredGPGPublicKeys(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, n, 0)
|
||||
|
||||
}
|
||||
// Bad case - empty ConfigMap
|
||||
{
|
||||
clientset := getGPGKeysClientset(gpgCMEmpty)
|
||||
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
|
||||
db := NewDB(testNamespace, settings, clientset)
|
||||
|
||||
// Key should be removed
|
||||
err := db.DeleteGPGPublicKey(context.Background(), "F7842A5CEAA9C0B1")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
// Code generated by mockery v1.1.2. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
|
|
@ -16,6 +16,38 @@ type ArgoDB struct {
|
|||
mock.Mock
|
||||
}
|
||||
|
||||
// AddGPGPublicKey provides a mock function with given fields: ctx, keyData
|
||||
func (_m *ArgoDB) AddGPGPublicKey(ctx context.Context, keyData string) (map[string]*v1alpha1.GnuPGPublicKey, []string, error) {
|
||||
ret := _m.Called(ctx, keyData)
|
||||
|
||||
var r0 map[string]*v1alpha1.GnuPGPublicKey
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) map[string]*v1alpha1.GnuPGPublicKey); ok {
|
||||
r0 = rf(ctx, keyData)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]*v1alpha1.GnuPGPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 []string
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) []string); ok {
|
||||
r1 = rf(ctx, keyData)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func(context.Context, string) error); ok {
|
||||
r2 = rf(ctx, keyData)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// CreateCluster provides a mock function with given fields: ctx, c
|
||||
func (_m *ArgoDB) CreateCluster(ctx context.Context, c *v1alpha1.Cluster) (*v1alpha1.Cluster, error) {
|
||||
ret := _m.Called(ctx, c)
|
||||
|
|
@ -122,6 +154,20 @@ func (_m *ArgoDB) DeleteCluster(ctx context.Context, server string) error {
|
|||
return r0
|
||||
}
|
||||
|
||||
// DeleteGPGPublicKey provides a mock function with given fields: ctx, keyID
|
||||
func (_m *ArgoDB) DeleteGPGPublicKey(ctx context.Context, keyID string) error {
|
||||
ret := _m.Called(ctx, keyID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, keyID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteRepository provides a mock function with given fields: ctx, name
|
||||
func (_m *ArgoDB) DeleteRepository(ctx context.Context, name string) error {
|
||||
ret := _m.Called(ctx, name)
|
||||
|
|
@ -242,6 +288,29 @@ func (_m *ArgoDB) ListClusters(ctx context.Context) (*v1alpha1.ClusterList, erro
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// ListConfiguredGPGPublicKeys provides a mock function with given fields: ctx
|
||||
func (_m *ArgoDB) ListConfiguredGPGPublicKeys(ctx context.Context) (map[string]*v1alpha1.GnuPGPublicKey, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 map[string]*v1alpha1.GnuPGPublicKey
|
||||
if rf, ok := ret.Get(0).(func(context.Context) map[string]*v1alpha1.GnuPGPublicKey); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]*v1alpha1.GnuPGPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ListHelmRepositories provides a mock function with given fields: ctx
|
||||
func (_m *ArgoDB) ListHelmRepositories(ctx context.Context) ([]*v1alpha1.Repository, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ type Client interface {
|
|||
LsLargeFiles() ([]string, error)
|
||||
CommitSHA() (string, error)
|
||||
RevisionMetadata(revision string) (*RevisionMetadata, error)
|
||||
VerifyCommitSignature(string) (string, error)
|
||||
}
|
||||
|
||||
// nativeGitClient implements Client interface using git CLI
|
||||
|
|
@ -439,6 +440,22 @@ func (m *nativeGitClient) RevisionMetadata(revision string) (*RevisionMetadata,
|
|||
return &RevisionMetadata{author, time.Unix(authorDateUnixTimestamp, 0), tags, message}, nil
|
||||
}
|
||||
|
||||
// VerifyCommitSignature Runs verify-commit on a given revision and returns the output
|
||||
func (m *nativeGitClient) VerifyCommitSignature(revision string) (string, error) {
|
||||
out, err := m.runGnuPGWrapper("git-verify-wrapper.sh", revision)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// runWrapper runs a custom command with all the semantics of running the Git client
|
||||
func (m *nativeGitClient) runGnuPGWrapper(wrapper string, args ...string) (string, error) {
|
||||
cmd := exec.Command(wrapper, args...)
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("GNUPGHOME=%s", common.GetGnuPGHomePath()))
|
||||
return m.runCmdOutput(cmd)
|
||||
}
|
||||
|
||||
// runCmd is a convenience function to run a command in a given directory and return its output
|
||||
func (m *nativeGitClient) runCmd(args ...string) (string, error) {
|
||||
cmd := exec.Command("git", args...)
|
||||
|
|
|
|||
|
|
@ -270,6 +270,45 @@ func TestLFSClient(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestVerifyCommitSignature(t *testing.T) {
|
||||
p, err := ioutil.TempDir("", "test-verify-commit-sig")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer os.RemoveAll(p)
|
||||
|
||||
client, err := NewClientExt("https://github.com/argoproj/argo-cd.git", p, NopCreds{}, false, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = client.Init()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = client.Fetch()
|
||||
assert.NoError(t, err)
|
||||
|
||||
commitSHA, err := client.LsRemote("HEAD")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = client.Checkout(commitSHA)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 28027897aad1262662096745f2ce2d4c74d02b7f is a commit that is signed in the repo
|
||||
// It doesn't matter whether we know the key or not at this stage
|
||||
{
|
||||
out, err := client.VerifyCommitSignature("28027897aad1262662096745f2ce2d4c74d02b7f")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, out)
|
||||
assert.Contains(t, out, "gpg: Signature made")
|
||||
}
|
||||
|
||||
// 85d660f0b967960becce3d49bd51c678ba2a5d24 is a commit that is not signed
|
||||
{
|
||||
out, err := client.VerifyCommitSignature("85d660f0b967960becce3d49bd51c678ba2a5d24")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFactory(t *testing.T) {
|
||||
addBinDirToPath := path.NewBinDirToPath()
|
||||
defer addBinDirToPath.Close()
|
||||
|
|
|
|||
|
|
@ -2,11 +2,8 @@
|
|||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
git "github.com/argoproj/argo-cd/util/git"
|
||||
)
|
||||
import git "github.com/argoproj/argo-cd/util/git"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
|
||||
// Client is an autogenerated mock type for the Client type
|
||||
type Client struct {
|
||||
|
|
@ -179,3 +176,24 @@ func (_m *Client) Root() string {
|
|||
|
||||
return r0
|
||||
}
|
||||
|
||||
// VerifyCommitSignature provides a mock function with given fields: _a0
|
||||
func (_m *Client) VerifyCommitSignature(_a0 string) (string, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
|
|
|||
682
util/gpg/gpg.go
Normal file
682
util/gpg/gpg.go
Normal file
|
|
@ -0,0 +1,682 @@
|
|||
package gpg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
executil "github.com/argoproj/gitops-engine/pkg/utils/exec"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
// Regular expression to match public key beginning
|
||||
var subTypeMatch = regexp.MustCompile(`^pub\s+([a-z0-9]+)\s\d+-\d+-\d+\s\[[A-Z]+\].*$`)
|
||||
|
||||
// Regular expression to match key ID output from gpg
|
||||
var keyIdMatch = regexp.MustCompile(`^\s+([0-9A-Za-z]+)\s*$`)
|
||||
|
||||
// Regular expression to match identity output from gpg
|
||||
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 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 --generate-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
|
||||
// creating the trustdb in a specific argocd-repo-server pod.
|
||||
var batchKeyCreateRecipe = `%no-protection
|
||||
%transient-key
|
||||
Key-Type: default
|
||||
Key-Length: 2048
|
||||
Key-Usage: sign
|
||||
Name-Real: Anon Ymous
|
||||
Name-Comment: ArgoCD key signing key
|
||||
Name-Email: noreply@argoproj.io
|
||||
Expire-Date: 6m
|
||||
%commit
|
||||
`
|
||||
|
||||
type PGPKeyID string
|
||||
|
||||
func isHexString(s string) bool {
|
||||
_, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if IsLongKeyID(k) {
|
||||
return k[24:]
|
||||
} else if IsShortKeyID(k) {
|
||||
return k
|
||||
}
|
||||
// Invalid key
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsLongKeyID returns true if the string represents a long key ID (aka fingerprint)
|
||||
func IsLongKeyID(k string) bool {
|
||||
if len(k) == 40 && isHexString(k) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsShortKeyID returns true if the string represents a short key ID
|
||||
func IsShortKeyID(k string) bool {
|
||||
if len(k) == 16 && isHexString(k) {
|
||||
return true
|
||||
} else {
|
||||
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"
|
||||
TrustNone = "never"
|
||||
TrustMarginal = "marginal"
|
||||
TrustFull = "full"
|
||||
TrustUltimate = "ultimate"
|
||||
)
|
||||
|
||||
// Key trust mappings
|
||||
var pgpTrustLevels = map[string]int{
|
||||
TrustUnknown: 2,
|
||||
TrustNone: 3,
|
||||
TrustMarginal: 4,
|
||||
TrustFull: 5,
|
||||
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(), fmt.Sprintf("GNUPGHOME=%s", common.GetGnuPGHomePath()))
|
||||
}
|
||||
|
||||
// Helper function to write some data to a temp file and return its path
|
||||
func writeKeyToFile(keyData string) (string, error) {
|
||||
f, err := ioutil.TempFile("", "gpg-public-key")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(f.Name(), []byte(keyData), 0600)
|
||||
if err != nil {
|
||||
os.Remove(f.Name())
|
||||
return "", err
|
||||
}
|
||||
f.Close()
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
// IsGPGEnabled returns true if GPG feature is enabled
|
||||
func IsGPGEnabled() bool {
|
||||
if en := os.Getenv("ARGOCD_GPG_ENABLED"); strings.ToLower(en) == "false" || strings.ToLower(en) == "no" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// InitializePGP will initialize a GnuPG working directory and also create a
|
||||
// transient private key so that the trust DB will work correctly.
|
||||
func InitializeGnuPG() error {
|
||||
|
||||
gnuPgHome := common.GetGnuPGHomePath()
|
||||
|
||||
// We only operate if ARGOCD_GNUPGHOME is set
|
||||
if gnuPgHome == "" {
|
||||
return fmt.Errorf("%s is not set; refusing to initialize", common.EnvGnuPGHome)
|
||||
}
|
||||
|
||||
// Directory set in ARGOCD_GNUPGHOME must exist and has to be a directory
|
||||
st, err := os.Stat(gnuPgHome)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !st.IsDir() {
|
||||
return fmt.Errorf("%s ('%s') does not point to a directory", common.EnvGnuPGHome, gnuPgHome)
|
||||
}
|
||||
|
||||
// Check for sane permissions as well (GPG will issue a warning otherwise)
|
||||
if st.Mode().Perm() != 0700 {
|
||||
return fmt.Errorf("%s at '%s' has too wide permissions, must be 0700", common.EnvGnuPGHome, gnuPgHome)
|
||||
}
|
||||
|
||||
_, err = os.Stat(path.Join(gnuPgHome, "trustdb.gpg"))
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// We can't initialize a second time
|
||||
return fmt.Errorf("%s at %s already initialized, can't initialize again.", common.EnvGnuPGHome, gnuPgHome)
|
||||
}
|
||||
|
||||
f, err := ioutil.TempFile("", "gpg-key-recipe")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
_, err = f.WriteString(batchKeyCreateRecipe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Close()
|
||||
|
||||
cmd := exec.Command("gpg", "--logger-fd", "1", "--batch", "--generate-key", f.Name())
|
||||
cmd.Env = getGPGEnviron()
|
||||
|
||||
_, err = executil.Run(cmd)
|
||||
return err
|
||||
}
|
||||
|
||||
func ParsePGPKeyBlock(keyFile string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func ImportPGPKeysFromString(keyData string) ([]*appsv1.GnuPGPublicKey, error) {
|
||||
f, err := ioutil.TempFile("", "gpg-key-import")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
_, err = f.WriteString(keyData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Close()
|
||||
return ImportPGPKeys(f.Name())
|
||||
}
|
||||
|
||||
// ImportPGPKey imports one or more keys from a file into the local keyring and optionally
|
||||
// signs them with the transient private key for leveraging the trust DB.
|
||||
func ImportPGPKeys(keyFile string) ([]*appsv1.GnuPGPublicKey, error) {
|
||||
keys := make([]*appsv1.GnuPGPublicKey, 0)
|
||||
|
||||
cmd := exec.Command("gpg", "--logger-fd", "1", "--import", keyFile)
|
||||
cmd.Env = getGPGEnviron()
|
||||
|
||||
out, err := executil.Run(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(out))
|
||||
for scanner.Scan() {
|
||||
if !strings.HasPrefix(scanner.Text(), "gpg: ") {
|
||||
continue
|
||||
}
|
||||
// We ignore lines that are not of interest
|
||||
token := importMatch.FindStringSubmatch(scanner.Text())
|
||||
if len(token) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := appsv1.GnuPGPublicKey{
|
||||
KeyID: token[1],
|
||||
Owner: token[2],
|
||||
// By default, trust level is unknown
|
||||
Trust: TrustUnknown,
|
||||
// Subtype is unknown at this point
|
||||
SubType: "unknown",
|
||||
Fingerprint: "",
|
||||
}
|
||||
|
||||
keys = append(keys, &key)
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func ValidatePGPKeysFromString(keyData string) (map[string]*appsv1.GnuPGPublicKey, error) {
|
||||
f, err := writeKeyToFile(keyData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(f)
|
||||
|
||||
return ValidatePGPKeys(f)
|
||||
}
|
||||
|
||||
// ValidatePGPKeys validates whether the keys in keyFile are valid PGP keys and can be imported
|
||||
// It does so by importing them into a temporary keyring. The returned keys are complete, that
|
||||
// is, they contain all relevant information
|
||||
func ValidatePGPKeys(keyFile string) (map[string]*appsv1.GnuPGPublicKey, error) {
|
||||
keys := make(map[string]*appsv1.GnuPGPublicKey)
|
||||
tempHome, err := ioutil.TempDir("", "gpg-verify-key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.RemoveAll(tempHome)
|
||||
|
||||
// Remember original GNUPGHOME, then set it to temp directory
|
||||
oldGPGHome := os.Getenv(common.EnvGnuPGHome)
|
||||
defer os.Setenv(common.EnvGnuPGHome, oldGPGHome)
|
||||
os.Setenv(common.EnvGnuPGHome, tempHome)
|
||||
|
||||
// Import they keys to our temporary keyring...
|
||||
_, err = ImportPGPKeys(keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ... and export them again, to get key data and fingerprint
|
||||
imported, err := GetInstalledPGPKeys(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, key := range imported {
|
||||
keys[key.KeyID] = key
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// SetPGPTrustLevel sets the given trust level on keys with specified key IDs
|
||||
func SetPGPTrustLevelById(kids []string, trustLevel string) error {
|
||||
keys := make([]*appsv1.GnuPGPublicKey, 0)
|
||||
for _, kid := range kids {
|
||||
keys = append(keys, &appsv1.GnuPGPublicKey{KeyID: kid})
|
||||
}
|
||||
return SetPGPTrustLevel(keys, trustLevel)
|
||||
}
|
||||
|
||||
// SetPGPTrustLevel sets the given trust level on specified keys
|
||||
func SetPGPTrustLevel(pgpKeys []*appsv1.GnuPGPublicKey, trustLevel string) error {
|
||||
trust, ok := pgpTrustLevels[trustLevel]
|
||||
if !ok {
|
||||
return fmt.Errorf("Unknown trust level: %s", trustLevel)
|
||||
}
|
||||
|
||||
// We need to store ownertrust specification in a temp file. Format is <fingerprint>:<level>
|
||||
f, err := ioutil.TempFile("", "gpg-key-fps")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
for _, k := range pgpKeys {
|
||||
_, err := f.WriteString(fmt.Sprintf("%s:%d\n", k.KeyID, trust))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
f.Close()
|
||||
|
||||
// Load ownertrust from the file we have constructed and instruct gpg to update the trustdb
|
||||
cmd := exec.Command("gpg", "--import-ownertrust", f.Name())
|
||||
cmd.Env = getGPGEnviron()
|
||||
|
||||
_, err = executil.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the trustdb once we updated the ownertrust, to prevent gpg to do it once we validate a signature
|
||||
cmd = exec.Command("gpg", "--update-trustdb")
|
||||
cmd.Env = getGPGEnviron()
|
||||
_, err = executil.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePGPKey deletes a key from our GnuPG key ring
|
||||
func DeletePGPKey(keyID string) error {
|
||||
args := append([]string{}, "--yes", "--batch", "--delete-keys", keyID)
|
||||
cmd := exec.Command("gpg", args...)
|
||||
cmd.Env = getGPGEnviron()
|
||||
|
||||
_, err := executil.Run(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSecretKey returns true if the keyID also has a private key in the keyring
|
||||
func IsSecretKey(keyID string) (bool, error) {
|
||||
args := append([]string{}, "--list-secret-keys", keyID)
|
||||
cmd := exec.Command("gpg-wrapper.sh", args...)
|
||||
cmd.Env = getGPGEnviron()
|
||||
out, err := executil.Run(cmd)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if strings.HasPrefix(out, "gpg: error reading key: No secret key") {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
args := append([]string{}, "--list-public-keys")
|
||||
// kids can contain an arbitrary list of key IDs we want to list. If empty, we list all keys.
|
||||
if len(kids) > 0 {
|
||||
args = append(args, kids...)
|
||||
}
|
||||
cmd := exec.Command("gpg", args...)
|
||||
cmd.Env = getGPGEnviron()
|
||||
|
||||
out, err := executil.Run(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(out))
|
||||
var curKey *appsv1.GnuPGPublicKey = nil
|
||||
for scanner.Scan() {
|
||||
if strings.HasPrefix(scanner.Text(), "pub ") {
|
||||
// This is the beginning of a new key, time to store the previously parsed one in our list and start fresh.
|
||||
if curKey != nil {
|
||||
keys = append(keys, curKey)
|
||||
curKey = nil
|
||||
}
|
||||
|
||||
key := appsv1.GnuPGPublicKey{}
|
||||
|
||||
// Second field in pub output denotes key sub type (cipher and length)
|
||||
token := subTypeMatch.FindStringSubmatch(scanner.Text())
|
||||
if len(token) != 2 {
|
||||
return nil, fmt.Errorf("Invalid line: %s (len=%d)", scanner.Text(), len(token))
|
||||
}
|
||||
key.SubType = token[1]
|
||||
|
||||
// Next line should be the key ID, no prefix
|
||||
if !scanner.Scan() {
|
||||
return nil, fmt.Errorf("Invalid output from gpg, end of text after primary key")
|
||||
}
|
||||
|
||||
token = keyIdMatch.FindStringSubmatch(scanner.Text())
|
||||
if len(token) != 2 {
|
||||
return nil, fmt.Errorf("Invalid output from gpg, no key ID for primary key")
|
||||
}
|
||||
|
||||
key.Fingerprint = token[1]
|
||||
// KeyID is just the last bytes of the fingerprint
|
||||
key.KeyID = token[1][24:]
|
||||
|
||||
if curKey == nil {
|
||||
curKey = &key
|
||||
}
|
||||
|
||||
// Next line should be UID
|
||||
if !scanner.Scan() {
|
||||
return nil, fmt.Errorf("Invalid output from gpg, end of text after key ID")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(scanner.Text(), "uid ") {
|
||||
return nil, fmt.Errorf("Invalid output from gpg, no identity for primary key")
|
||||
}
|
||||
|
||||
token = uidMatch.FindStringSubmatch(scanner.Text())
|
||||
|
||||
if len(token) < 3 {
|
||||
return nil, fmt.Errorf("Malformed identity line: %s (len=%d)", scanner.Text(), len(token))
|
||||
}
|
||||
|
||||
// Store trust level
|
||||
key.Trust = token[1]
|
||||
|
||||
// Identity - we are only interested in the first uid
|
||||
key.Owner = token[2]
|
||||
}
|
||||
}
|
||||
|
||||
// Also store the last processed key into our list to be returned
|
||||
if curKey != nil {
|
||||
keys = append(keys, curKey)
|
||||
}
|
||||
|
||||
// We need to get the final key for each imported key, so we run --export on each key
|
||||
for _, key := range keys {
|
||||
cmd := exec.Command("gpg", "-a", "--export", key.KeyID)
|
||||
cmd.Env = getGPGEnviron()
|
||||
|
||||
out, err := executil.Run(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key.KeyData = out
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// ParsePGPCommitSignature parses the output of "git verify-commit" and returns the result
|
||||
func ParseGitCommitVerification(signature string) (PGPVerifyResult, error) {
|
||||
result := PGPVerifyResult{Result: VerifyResultUnknown}
|
||||
parseOk := false
|
||||
linesParsed := 0
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(signature))
|
||||
for scanner.Scan() && linesParsed < MaxVerificationLinesToParse {
|
||||
linesParsed += 1
|
||||
|
||||
// Indicating the beginning of a signature
|
||||
start := verificationStartMatch.FindStringSubmatch(scanner.Text())
|
||||
if len(start) == 2 {
|
||||
result.Date = start[1]
|
||||
if !scanner.Scan() {
|
||||
return PGPVerifyResult{}, fmt.Errorf("Unexpected end-of-file while parsing commit verification output.")
|
||||
}
|
||||
|
||||
linesParsed += 1
|
||||
|
||||
// What key has made the signature?
|
||||
keyID := verificationKeyIDMatch.FindStringSubmatch(scanner.Text())
|
||||
if len(keyID) != 3 {
|
||||
return PGPVerifyResult{}, fmt.Errorf("Could not parse key ID of commit verification output.")
|
||||
}
|
||||
|
||||
result.Cipher = keyID[1]
|
||||
result.KeyID = KeyID(keyID[2])
|
||||
if result.KeyID == "" {
|
||||
return PGPVerifyResult{}, fmt.Errorf("Invalid PGP key ID found in verification result: %s", result.KeyID)
|
||||
}
|
||||
|
||||
// What was the result of signature verification?
|
||||
if !scanner.Scan() {
|
||||
return PGPVerifyResult{}, fmt.Errorf("Unexpected end-of-file while parsing commit verification output.")
|
||||
}
|
||||
|
||||
linesParsed += 1
|
||||
|
||||
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 PGPVerifyResult{}, fmt.Errorf("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 successfull - return result
|
||||
return result, nil
|
||||
} else if linesParsed >= MaxVerificationLinesToParse {
|
||||
// Too many output lines, return error
|
||||
return PGPVerifyResult{}, fmt.Errorf("Too many lines of gpg verify-commit output, abort.")
|
||||
} else {
|
||||
// No data found, return error
|
||||
return PGPVerifyResult{}, fmt.Errorf("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
|
||||
// in the keyring will be installed to the keyring, files that exist in the keyring but do not exist in
|
||||
// the directory will be deleted.
|
||||
func SyncKeyRingFromDirectory(basePath string) ([]string, []string, error) {
|
||||
configured := make(map[string]interface{})
|
||||
newKeys := make([]string, 0)
|
||||
fingerprints := make([]string, 0)
|
||||
removedKeys := make([]string, 0)
|
||||
st, err := os.Stat(basePath)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !st.IsDir() {
|
||||
return nil, nil, fmt.Errorf("%s is not a directory", basePath)
|
||||
}
|
||||
|
||||
// Collect configuration, i.e. files in basePath
|
||||
err = filepath.Walk(basePath, func(path string, fi os.FileInfo, err error) error {
|
||||
if IsShortKeyID(fi.Name()) {
|
||||
configured[fi.Name()] = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Collect GPG keys installed in the key ring
|
||||
installed := make(map[string]*appsv1.GnuPGPublicKey)
|
||||
keys, err := GetInstalledPGPKeys(nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, v := range keys {
|
||||
installed[v.KeyID] = v
|
||||
}
|
||||
|
||||
// First, add all keys that are found in the configuration but are not yet in the keyring
|
||||
for key := range configured {
|
||||
if _, ok := installed[key]; !ok {
|
||||
addedKey, err := ImportPGPKeys(path.Join(basePath, key))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(addedKey) != 1 {
|
||||
return nil, nil, fmt.Errorf("Invalid key found in %s", path.Join(basePath, key))
|
||||
}
|
||||
importedKey, err := GetInstalledPGPKeys([]string{addedKey[0].KeyID})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if len(importedKey) != 1 {
|
||||
return nil, nil, fmt.Errorf("Could not get details of imported key ID %s", importedKey)
|
||||
}
|
||||
newKeys = append(newKeys, key)
|
||||
fingerprints = append(fingerprints, importedKey[0].Fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all keys from the keyring that are not found in the configuration anymore.
|
||||
for key := range installed {
|
||||
secret, err := IsSecretKey(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if _, ok := configured[key]; !ok && !secret {
|
||||
err := DeletePGPKey(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
removedKeys = append(removedKeys, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Update owner trust for new keys
|
||||
if len(fingerprints) > 0 {
|
||||
_ = SetPGPTrustLevelById(fingerprints, TrustUltimate)
|
||||
}
|
||||
|
||||
return newKeys, removedKeys, err
|
||||
}
|
||||
580
util/gpg/gpg_test.go
Normal file
580
util/gpg/gpg_test.go
Normal file
|
|
@ -0,0 +1,580 @@
|
|||
package gpg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
)
|
||||
|
||||
const (
|
||||
longKeyID = "5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB23"
|
||||
shortKeyID = "4AEE18F83AFDEB23"
|
||||
)
|
||||
|
||||
var syncTestSources = map[string]string{
|
||||
"F7842A5CEAA9C0B1": "testdata/janedoe.asc",
|
||||
"FDC79815400D88A9": "testdata/johndoe.asc",
|
||||
"4AEE18F83AFDEB23": "testdata/github.asc",
|
||||
}
|
||||
|
||||
// Helper function to create temporary GNUPGHOME
|
||||
func initTempDir() string {
|
||||
p, err := ioutil.TempDir("", "gpg-test")
|
||||
if err != nil {
|
||||
// makes no sense to continue test without temp dir
|
||||
panic(err.Error())
|
||||
}
|
||||
fmt.Printf("-> Using %s as GNUPGHOME\n", p)
|
||||
os.Setenv(common.EnvGnuPGHome, p)
|
||||
return p
|
||||
}
|
||||
|
||||
func Test_IsGPGEnabled(t *testing.T) {
|
||||
os.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
assert.True(t, IsGPGEnabled())
|
||||
os.Setenv("ARGOCD_GPG_ENABLED", "false")
|
||||
assert.False(t, IsGPGEnabled())
|
||||
os.Setenv("ARGOCD_GPG_ENABLED", "")
|
||||
assert.True(t, IsGPGEnabled())
|
||||
}
|
||||
|
||||
func Test_GPG_InitializeGnuPG(t *testing.T) {
|
||||
p := initTempDir()
|
||||
defer os.RemoveAll(p)
|
||||
|
||||
// First run should initialize fine
|
||||
err := InitializeGnuPG()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// We should have exactly one public key with ultimate trust (our own) in the keyring
|
||||
keys, err := GetInstalledPGPKeys(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
assert.Equal(t, keys[0].Trust, "ultimate")
|
||||
|
||||
// Second run should return error
|
||||
err = InitializeGnuPG()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already initialized")
|
||||
|
||||
// GNUPGHOME is a file - we need to error out
|
||||
f, err := ioutil.TempFile("", "gpg-test")
|
||||
assert.NoError(t, err)
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
os.Setenv(common.EnvGnuPGHome, f.Name())
|
||||
err = InitializeGnuPG()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "does not point to a directory")
|
||||
|
||||
// Unaccessible GNUPGHOME
|
||||
p = initTempDir()
|
||||
defer os.RemoveAll(p)
|
||||
fp := fmt.Sprintf("%s/gpg", p)
|
||||
err = os.Mkdir(fp, 0000)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
os.Setenv(common.EnvGnuPGHome, fp)
|
||||
err = InitializeGnuPG()
|
||||
assert.Error(t, err)
|
||||
// Restore permissions so path can be deleted
|
||||
err = os.Chmod(fp, 0700)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
// GNUPGHOME with too wide permissions
|
||||
p = initTempDir()
|
||||
defer os.RemoveAll(p)
|
||||
err = os.Chmod(p, 0777)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
os.Setenv(common.EnvGnuPGHome, p)
|
||||
err = InitializeGnuPG()
|
||||
assert.Error(t, err)
|
||||
|
||||
}
|
||||
|
||||
func Test_GPG_KeyManagement(t *testing.T) {
|
||||
p := initTempDir()
|
||||
defer os.RemoveAll(p)
|
||||
|
||||
err := InitializeGnuPG()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Import a single good key
|
||||
keys, err := ImportPGPKeys("testdata/github.asc")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
assert.Equal(t, "4AEE18F83AFDEB23", keys[0].KeyID)
|
||||
assert.Contains(t, keys[0].Owner, "noreply@github.com")
|
||||
assert.Equal(t, "unknown", keys[0].Trust)
|
||||
assert.Equal(t, "unknown", keys[0].SubType)
|
||||
|
||||
kids := make([]string, 0)
|
||||
importedKeyId := keys[0].KeyID
|
||||
|
||||
// We should have a total of 2 keys in the keyring now
|
||||
{
|
||||
keys, err := GetInstalledPGPKeys(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 2)
|
||||
}
|
||||
|
||||
// We should now have that key in our keyring with unknown trust (trustdb not updated)
|
||||
{
|
||||
keys, err := GetInstalledPGPKeys([]string{importedKeyId})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
assert.Equal(t, "4AEE18F83AFDEB23", keys[0].KeyID)
|
||||
assert.Contains(t, keys[0].Owner, "noreply@github.com")
|
||||
assert.Equal(t, "unknown", keys[0].Trust)
|
||||
assert.Equal(t, "rsa2048", keys[0].SubType)
|
||||
kids = append(kids, keys[0].Fingerprint)
|
||||
}
|
||||
|
||||
assert.Len(t, kids, 1)
|
||||
|
||||
// Set trust level for our key and check the result
|
||||
{
|
||||
err := SetPGPTrustLevelById(kids, "ultimate")
|
||||
assert.NoError(t, err)
|
||||
keys, err := GetInstalledPGPKeys(kids)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
assert.Equal(t, kids[0], keys[0].Fingerprint)
|
||||
assert.Equal(t, "ultimate", keys[0].Trust)
|
||||
}
|
||||
|
||||
// Import garbage - error expected
|
||||
keys, err = ImportPGPKeys("testdata/garbage.asc")
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, keys, 0)
|
||||
|
||||
// We should still have a total of 2 keys in the keyring now
|
||||
{
|
||||
keys, err := GetInstalledPGPKeys(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 2)
|
||||
}
|
||||
|
||||
// Delete previously imported public key
|
||||
{
|
||||
err := DeletePGPKey(importedKeyId)
|
||||
assert.NoError(t, err)
|
||||
keys, err := GetInstalledPGPKeys(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
}
|
||||
|
||||
// Delete non-existing key
|
||||
{
|
||||
err := DeletePGPKey(importedKeyId)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// Import multiple keys
|
||||
{
|
||||
keys, err := ImportPGPKeys("testdata/multi.asc")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 2)
|
||||
assert.Contains(t, keys[0].Owner, "john.doe@example.com")
|
||||
assert.Contains(t, keys[1].Owner, "jane.doe@example.com")
|
||||
}
|
||||
|
||||
// Check if they were really imported
|
||||
{
|
||||
keys, err := GetInstalledPGPKeys(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 3)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_ImportPGPKeysFromString(t *testing.T) {
|
||||
p := initTempDir()
|
||||
defer os.RemoveAll(p)
|
||||
|
||||
err := InitializeGnuPG()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Import a single good key
|
||||
keys, err := ImportPGPKeysFromString(test.MustLoadFileToString("testdata/github.asc"))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
assert.Equal(t, "4AEE18F83AFDEB23", keys[0].KeyID)
|
||||
assert.Contains(t, keys[0].Owner, "noreply@github.com")
|
||||
assert.Equal(t, "unknown", keys[0].Trust)
|
||||
assert.Equal(t, "unknown", keys[0].SubType)
|
||||
|
||||
}
|
||||
|
||||
func Test_ValidateGPGKeysFromString(t *testing.T) {
|
||||
p := initTempDir()
|
||||
defer os.RemoveAll(p)
|
||||
|
||||
err := InitializeGnuPG()
|
||||
assert.NoError(t, err)
|
||||
|
||||
{
|
||||
keyData := test.MustLoadFileToString("testdata/github.asc")
|
||||
keys, err := ValidatePGPKeysFromString(keyData)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
}
|
||||
|
||||
{
|
||||
keyData := test.MustLoadFileToString("testdata/multi.asc")
|
||||
keys, err := ValidatePGPKeysFromString(keyData)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_ValidateGPGKeys(t *testing.T) {
|
||||
p := initTempDir()
|
||||
defer os.RemoveAll(p)
|
||||
|
||||
err := InitializeGnuPG()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Validation good case - 1 key
|
||||
{
|
||||
keys, err := ValidatePGPKeys("testdata/github.asc")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
assert.Contains(t, keys, "4AEE18F83AFDEB23")
|
||||
}
|
||||
|
||||
// Validation bad case
|
||||
{
|
||||
keys, err := ValidatePGPKeys("testdata/garbage.asc")
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, keys, 0)
|
||||
}
|
||||
|
||||
// We should still have a total of 1 keys in the keyring now
|
||||
{
|
||||
keys, err := GetInstalledPGPKeys(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GPG_ParseGitCommitVerification(t *testing.T) {
|
||||
p := initTempDir()
|
||||
defer os.RemoveAll(p)
|
||||
|
||||
err := InitializeGnuPG()
|
||||
assert.NoError(t, err)
|
||||
|
||||
keys, err := ImportPGPKeys("testdata/github.asc")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
|
||||
// Good case
|
||||
{
|
||||
c, err := ioutil.ReadFile("testdata/good_signature.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res, err := ParseGitCommitVerification(string(c))
|
||||
assert.NoError(t, err)
|
||||
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 := ioutil.ReadFile("testdata/unknown_signature.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res, err := ParseGitCommitVerification(string(c))
|
||||
assert.NoError(t, err)
|
||||
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 := ioutil.ReadFile("testdata/bad_signature_bad.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
res, err := ParseGitCommitVerification(string(c))
|
||||
assert.NoError(t, err)
|
||||
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 := ioutil.ReadFile("testdata/bad_signature_manipulated.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
_, err = ParseGitCommitVerification(string(c))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Could not parse output")
|
||||
}
|
||||
|
||||
// Bad case: Incomplete signature data #1
|
||||
{
|
||||
c, err := ioutil.ReadFile("testdata/bad_signature_preeof1.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
_, err = ParseGitCommitVerification(string(c))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "end-of-file")
|
||||
}
|
||||
|
||||
// Bad case: Incomplete signature data #2
|
||||
{
|
||||
c, err := ioutil.ReadFile("testdata/bad_signature_preeof2.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
_, err = ParseGitCommitVerification(string(c))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "end-of-file")
|
||||
}
|
||||
|
||||
// Bad case: No signature data #1
|
||||
{
|
||||
c, err := ioutil.ReadFile("testdata/bad_signature_nodata.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
_, err = ParseGitCommitVerification(string(c))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no verification data found")
|
||||
}
|
||||
|
||||
// Bad case: Malformed signature data #1
|
||||
{
|
||||
c, err := ioutil.ReadFile("testdata/bad_signature_malformed1.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
_, err = ParseGitCommitVerification(string(c))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no verification data found")
|
||||
}
|
||||
|
||||
// Bad case: Malformed signature data #2
|
||||
{
|
||||
c, err := ioutil.ReadFile("testdata/bad_signature_malformed2.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
_, err = ParseGitCommitVerification(string(c))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Could not parse key ID")
|
||||
}
|
||||
|
||||
// Bad case: Malformed signature data #3
|
||||
{
|
||||
c, err := ioutil.ReadFile("testdata/bad_signature_malformed3.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
_, err = ParseGitCommitVerification(string(c))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Could not parse result of verify")
|
||||
}
|
||||
|
||||
// Bad case: Invalid key ID in signature
|
||||
{
|
||||
c, err := ioutil.ReadFile("testdata/bad_signature_badkeyid.txt")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
_, err = ParseGitCommitVerification(string(c))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Invalid PGP key ID")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GetGnuPGHomePath(t *testing.T) {
|
||||
{
|
||||
os.Setenv(common.EnvGnuPGHome, "")
|
||||
p := common.GetGnuPGHomePath()
|
||||
assert.Equal(t, common.DefaultGnuPgHomePath, p)
|
||||
}
|
||||
{
|
||||
os.Setenv(common.EnvGnuPGHome, "/tmp/gpghome")
|
||||
p := common.GetGnuPGHomePath()
|
||||
assert.Equal(t, "/tmp/gpghome", p)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_KeyID(t *testing.T) {
|
||||
// Good case - long key ID (aka fingerprint) to short key ID
|
||||
{
|
||||
res := KeyID(longKeyID)
|
||||
assert.Equal(t, shortKeyID, res)
|
||||
}
|
||||
// Good case - short key ID remains same
|
||||
{
|
||||
res := KeyID(shortKeyID)
|
||||
assert.Equal(t, shortKeyID, res)
|
||||
}
|
||||
// Bad case - key ID too short
|
||||
{
|
||||
keyID := "AEE18F83AFDEB23"
|
||||
res := KeyID(keyID)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
// Bad case - key ID too long
|
||||
{
|
||||
keyID := "5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB2323"
|
||||
res := KeyID(keyID)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
// Bad case - right length, but not hex string
|
||||
{
|
||||
keyID := "abcdefghijklmn"
|
||||
res := KeyID(keyID)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IsShortKeyID(t *testing.T) {
|
||||
assert.True(t, IsShortKeyID(shortKeyID))
|
||||
assert.False(t, IsShortKeyID(longKeyID))
|
||||
assert.False(t, IsShortKeyID("ab"))
|
||||
}
|
||||
func Test_IsLongKeyID(t *testing.T) {
|
||||
assert.True(t, IsLongKeyID(longKeyID))
|
||||
assert.False(t, IsLongKeyID(shortKeyID))
|
||||
assert.False(t, IsLongKeyID(longKeyID+"a"))
|
||||
}
|
||||
|
||||
func Test_isHexString(t *testing.T) {
|
||||
assert.True(t, isHexString("ab0099"))
|
||||
assert.True(t, isHexString("AB0099"))
|
||||
assert.False(t, isHexString("foobar"))
|
||||
}
|
||||
|
||||
func Test_IsSecretKey(t *testing.T) {
|
||||
p := initTempDir()
|
||||
defer os.RemoveAll(p)
|
||||
|
||||
// First run should initialize fine
|
||||
err := InitializeGnuPG()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// We should have exactly one public key with ultimate trust (our own) in the keyring
|
||||
keys, err := GetInstalledPGPKeys(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
assert.Equal(t, keys[0].Trust, "ultimate")
|
||||
|
||||
{
|
||||
secret, err := IsSecretKey(keys[0].KeyID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, secret)
|
||||
}
|
||||
|
||||
{
|
||||
secret, err := IsSecretKey("invalid")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, secret)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_SyncKeyRingFromDirectory(t *testing.T) {
|
||||
p := initTempDir()
|
||||
defer os.RemoveAll(p)
|
||||
|
||||
// First run should initialize fine
|
||||
err := InitializeGnuPG()
|
||||
assert.NoError(t, err)
|
||||
|
||||
tempDir, err := ioutil.TempDir("", "gpg-sync-test")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
{
|
||||
new, removed, err := SyncKeyRingFromDirectory(tempDir)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, new, 0)
|
||||
assert.Len(t, removed, 0)
|
||||
}
|
||||
|
||||
{
|
||||
for k, v := range syncTestSources {
|
||||
src, err := os.Open(v)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer src.Close()
|
||||
dst, err := os.Create(path.Join(tempDir, k))
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer dst.Close()
|
||||
_, err = io.Copy(dst, src)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
dst.Close()
|
||||
}
|
||||
|
||||
new, removed, err := SyncKeyRingFromDirectory(tempDir)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, new, 3)
|
||||
assert.Len(t, removed, 0)
|
||||
|
||||
installed, err := GetInstalledPGPKeys(new)
|
||||
assert.NoError(t, err)
|
||||
for _, k := range installed {
|
||||
assert.Contains(t, new, k.KeyID)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
err := os.Remove(path.Join(tempDir, "4AEE18F83AFDEB23"))
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
new, removed, err := SyncKeyRingFromDirectory(tempDir)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, new, 0)
|
||||
assert.Len(t, removed, 1)
|
||||
|
||||
installed, err := GetInstalledPGPKeys(new)
|
||||
assert.NoError(t, err)
|
||||
for _, k := range installed {
|
||||
assert.NotEqual(t, k.KeyID, removed[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
3
util/gpg/testdata/bad_signature_bad.txt
vendored
Normal file
3
util/gpg/testdata/bad_signature_bad.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
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
Normal file
3
util/gpg/testdata/bad_signature_badkeyid.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
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]
|
||||
3
util/gpg/testdata/bad_signature_malformed1.txt
vendored
Normal file
3
util/gpg/testdata/bad_signature_malformed1.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
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]
|
||||
3
util/gpg/testdata/bad_signature_malformed2.txt
vendored
Normal file
3
util/gpg/testdata/bad_signature_malformed2.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
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]
|
||||
3
util/gpg/testdata/bad_signature_malformed3.txt
vendored
Normal file
3
util/gpg/testdata/bad_signature_malformed3.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
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]
|
||||
6
util/gpg/testdata/bad_signature_manipulated.txt
vendored
Normal file
6
util/gpg/testdata/bad_signature_manipulated.txt
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
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
Normal file
3
util/gpg/testdata/bad_signature_nodata.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Lorem ipsum
|
||||
Lorem ipsum
|
||||
Lorem ipsum
|
||||
2
util/gpg/testdata/bad_signature_preeof1.txt
vendored
Normal file
2
util/gpg/testdata/bad_signature_preeof1.txt
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
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
Normal file
1
util/gpg/testdata/bad_signature_preeof2.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
|
||||
16
util/gpg/testdata/garbage.asc
vendored
Normal file
16
util/gpg/testdata/garbage.asc
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBFmUaEEBCACzXTDt6ZnyaVtueZASBzgnAmK13q9Urgch+sKYeIhdymjuMQta
|
||||
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-----
|
||||
17
util/gpg/testdata/github.asc
vendored
Normal file
17
util/gpg/testdata/github.asc
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
-----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-----
|
||||
3
util/gpg/testdata/good_signature.txt
vendored
Normal file
3
util/gpg/testdata/good_signature.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
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]
|
||||
30
util/gpg/testdata/janedoe.asc
vendored
Normal file
30
util/gpg/testdata/janedoe.asc
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBF5izVcBCADKkNZwGmtcTR7TN1tuC326+oXNewWRraKdnxWiKXW1gUROBDiW
|
||||
Pic9hImYYjkyt6dz4DkAB/qJfAiRTZG/zz/qnTgbrzK9j3v4TlBTUcTtCI4fF/Sh
|
||||
zutKpaIfWFDelKSIoWRh/gY6LrtnXm+PRLTckzQxUP71HrHlFFk3462+Ph+7V3z5
|
||||
PrUZvbv+wJ3U5GdhhYEIBpq2fkvv2K9l9MFVWXcH7mDLxX7p/Q8OaHaSsdTtpBpk
|
||||
y4IuA0RQiej0gXAEPuoO/TXKwtZ6G7eFjtcndomM2H3N7oYqZZuNW8lU+zcaV8HW
|
||||
KlwNZFvnkRAO05zCtN8ljUTWkZwM8k6BOp3bABEBAAG0H0phbmUgRG9lIDxqYW5l
|
||||
LmRvZUBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQQM1Ty6UL70MGRjStf3hCpc6qnA
|
||||
sQUCXmLNVwIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD3hCpc
|
||||
6qnAsT7uCACeKa0jKSzGmjVhxzTT8uO1bxZXzTLi2vDQcqFBVYUBe0TXgf9I8+0s
|
||||
WkvZfvZw9Mju3bxY+Tp1/e7+nKsEkQO/7rRureOa7OF/D5jJNX1QUNqUFF6LCPAB
|
||||
P5RroHS3uGfdCHKyj84jrZPAhTDPMyYlrWkv8EX2YOT6dlnxgElIdc3WcwJSAtFT
|
||||
WQXoY/kHmjoUe8c8NJFN1nwEHzbKtjsnkcXvs7XruUhhmqsizyCyrIS9We4Sl5r+
|
||||
4zKi10FKoN/kzCjU3EHOFiQ8/l5rKTMM1lAN4q3Wyq2xeqyJ+UDx7hOGnnxKjeX9
|
||||
uLay5cAy7XhOwghIQKCXtcd3T/EzlYQNuQENBF5izVcBCACiADPuJIRFkIuMLiov
|
||||
rWCAtlXt3OyyZrchtRDzxLJW6nL6vaMoJ7nUabD6mlv9mfWRLG4exUID6632/mXb
|
||||
lVcPYU4ZQM9HFutwi8cgq2SuiX/UJM0deJzmiQKxMNO4hUf7eQU7227jRdxkWaOj
|
||||
zN7xdayH6yldVyrPWQM8i4qmpsGPZ3/EYswDhxcPYPhkA1gW1tgaUxWf/k9U1+GY
|
||||
myaAI71pImRExUIc0pIv44IdGQRU3iPusgljDDXgPVhwF1EmAqFQ6aIM32h6x3WM
|
||||
T6u6OtWfGUksG3RBv3M380Tegppzk2X+2V38YaJH4u/jNUXhwbu/9yR4xuA02/Z1
|
||||
E44bABEBAAGJATwEGAEIACYWIQQM1Ty6UL70MGRjStf3hCpc6qnAsQUCXmLNVwIb
|
||||
DAUJA8JnAAAKCRD3hCpc6qnAsWb/CACKcrLYF4yK+vhdlTJw65znSBjIw16iH5SV
|
||||
yd+z3MmiQzlqpjxfMg7iJG1wDNl4wDRa3QipRrVhKvc8wRuUK7xUKUcbIJDBmoTc
|
||||
Mj5iicPucr1WQvv9OY3wXBBoWxdPgxWRBglbuZmp7s2D3ixbrd3nxGisFGXIkOAe
|
||||
WtOibFHnnaKwsM+xqpYDwn4PL/DYWuh0gT5RW6JG1PWdGWMr1CHjV8VyPD3dR59l
|
||||
976sRGz2oyu5gefZHtRecI65/BHtKtdf6ZZaonNXilK1XVrD2hahn0CKwumeQtS4
|
||||
kx/hB+SPrcCgCdUD7FFlMA3kGZ7rBKMkHVxoOondTAJFUw4TAE6j
|
||||
=ygiZ
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
30
util/gpg/testdata/johndoe.asc
vendored
Normal file
30
util/gpg/testdata/johndoe.asc
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBF5izSABCADAED7eSbx+ol3TLt/fJ6UZciaItts4Rar83bj8LPTFZebWTHzy
|
||||
m0zoNdU3UrH3I8iWhoUUE1voqp2Hs3GEX3fHK70BodhGkGl5W931l8yYqTVlLYhE
|
||||
8MxwWZKwh3phK6Wcm9GUEA3BQr5rNApWwUfgCK8NHRl2Kmb5ujmPgoap2RsH6Fpn
|
||||
85gaCfUOvTV7jAZtY+LU84ZsVh0TcNoA4UieYHYWvXtYci9C0EkVbjpoRhZOkv5h
|
||||
oQSBm/5Kfv+d7kZUluBsm1yyXfdHJBVuNYd7SpHe6PO3+eQ/JgqlRSfs1UBKYgx3
|
||||
Sxapy16hm8vVAzE9vnxB87z0+kS0uc0Ri+abABEBAAG0H0pvaG4gRG9lIDxqb2hu
|
||||
LmRvZUBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQRG7spZs6IkAFi4SpP9x5gVQA2I
|
||||
qQUCXmLNIAIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9x5gV
|
||||
QA2IqS84CACXKj4xJ+UAkh5q/M0jXC9JxzQu8JtVE7cGTTFyjLBApGmtfa5RtEct
|
||||
QShhpdVhpuh2DhsySoza6acvwaP356HywFH64Q0MXo98XosEnSwab74k1yyd2QPR
|
||||
u+kIskEbfs/j6e5uYpqf0tCvXsxIywktGcdvLE/98ISXqHS8R1uCuMrfWR9Rrz/b
|
||||
8k4NY5u6IZCa+HmrZ7v3K4s1XaHbSJaz5MzJI2kFT6Ai485KBf7Iof5llr9x5U0L
|
||||
rEiH1u2xIh64WvqO6u6xqxas1ewzuI6tGECU2sllZxPIt6/onCZy9LnOjJOhEAyT
|
||||
P7N+q5jsF+NvqvCame9hmYDSfUv6TP9IuQENBF5izSABCACv6y8rmRC0otzl7A9p
|
||||
yfoNH9FNpLaiYuT6XUMSSC97TG1jjPZ3+6TP1Ff6nEwDxf57zRq8yJZO8LMRXwyA
|
||||
kIT6ZPB9lY4Z6qy1TZAd2/UVG6KR9kml+S/hOo2Y9WAz8tDpYM9rGieIW+LXcueK
|
||||
lkI0TYS7FX49UFB/hXJMnnOhzZxihVo/g1rlAPLsxE2i/1TVmDD0EOMwiuOBwoyN
|
||||
UurJq41sXsxYZQFAjCbUfuvWgXjM/ir97Rr8Vca5SjGNf9C4yLsDGl/eKfKPLUwP
|
||||
7cgnq/pSpVaWDEAb6DyU8ttY7zZQOQjT5Gwfggxzz9U4qUOOtFkQ6piQoe1Lyzi8
|
||||
6cHZABEBAAGJATwEGAEIACYWIQRG7spZs6IkAFi4SpP9x5gVQA2IqQUCXmLNIAIb
|
||||
DAUJA8JnAAAKCRD9x5gVQA2IqX09CACTALlaIOxa9VlBrhaj5bHkMwXJG3DDDLm1
|
||||
9aJDJfwjqEnCFT7SCggZFCBpu3PqEkq8jHGC/gnWcDoPhWtMldBRVb3MjsxjOi9t
|
||||
Lk39XcoQOgYo6aFMD1Ughbg+P2QrQwvLhtIl7134MUiB65IsDRLrjXkkMhVEe1Um
|
||||
0yL4doZPxZ/jm+dGxtFWcAXWBTL4lzE3fWCwMmygiuxljLl9n67glsZG7isRVMfY
|
||||
U9O8kAMRoMCiktnIe+Ecw1RmAmjgDmA/jTKPGuJRTj/WO5LtWwHUXa7jptJLU1tZ
|
||||
kdXx0SzOArmDG0dMwggSm9ms4Z8FT+XXe1BWKV1jLTDqBRP1z/KD
|
||||
=8jOZ
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
56
util/gpg/testdata/multi.asc
vendored
Normal file
56
util/gpg/testdata/multi.asc
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBF5izSABCADAED7eSbx+ol3TLt/fJ6UZciaItts4Rar83bj8LPTFZebWTHzy
|
||||
m0zoNdU3UrH3I8iWhoUUE1voqp2Hs3GEX3fHK70BodhGkGl5W931l8yYqTVlLYhE
|
||||
8MxwWZKwh3phK6Wcm9GUEA3BQr5rNApWwUfgCK8NHRl2Kmb5ujmPgoap2RsH6Fpn
|
||||
85gaCfUOvTV7jAZtY+LU84ZsVh0TcNoA4UieYHYWvXtYci9C0EkVbjpoRhZOkv5h
|
||||
oQSBm/5Kfv+d7kZUluBsm1yyXfdHJBVuNYd7SpHe6PO3+eQ/JgqlRSfs1UBKYgx3
|
||||
Sxapy16hm8vVAzE9vnxB87z0+kS0uc0Ri+abABEBAAG0H0pvaG4gRG9lIDxqb2hu
|
||||
LmRvZUBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQRG7spZs6IkAFi4SpP9x5gVQA2I
|
||||
qQUCXmLNIAIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9x5gV
|
||||
QA2IqS84CACXKj4xJ+UAkh5q/M0jXC9JxzQu8JtVE7cGTTFyjLBApGmtfa5RtEct
|
||||
QShhpdVhpuh2DhsySoza6acvwaP356HywFH64Q0MXo98XosEnSwab74k1yyd2QPR
|
||||
u+kIskEbfs/j6e5uYpqf0tCvXsxIywktGcdvLE/98ISXqHS8R1uCuMrfWR9Rrz/b
|
||||
8k4NY5u6IZCa+HmrZ7v3K4s1XaHbSJaz5MzJI2kFT6Ai485KBf7Iof5llr9x5U0L
|
||||
rEiH1u2xIh64WvqO6u6xqxas1ewzuI6tGECU2sllZxPIt6/onCZy9LnOjJOhEAyT
|
||||
P7N+q5jsF+NvqvCame9hmYDSfUv6TP9IuQENBF5izSABCACv6y8rmRC0otzl7A9p
|
||||
yfoNH9FNpLaiYuT6XUMSSC97TG1jjPZ3+6TP1Ff6nEwDxf57zRq8yJZO8LMRXwyA
|
||||
kIT6ZPB9lY4Z6qy1TZAd2/UVG6KR9kml+S/hOo2Y9WAz8tDpYM9rGieIW+LXcueK
|
||||
lkI0TYS7FX49UFB/hXJMnnOhzZxihVo/g1rlAPLsxE2i/1TVmDD0EOMwiuOBwoyN
|
||||
UurJq41sXsxYZQFAjCbUfuvWgXjM/ir97Rr8Vca5SjGNf9C4yLsDGl/eKfKPLUwP
|
||||
7cgnq/pSpVaWDEAb6DyU8ttY7zZQOQjT5Gwfggxzz9U4qUOOtFkQ6piQoe1Lyzi8
|
||||
6cHZABEBAAGJATwEGAEIACYWIQRG7spZs6IkAFi4SpP9x5gVQA2IqQUCXmLNIAIb
|
||||
DAUJA8JnAAAKCRD9x5gVQA2IqX09CACTALlaIOxa9VlBrhaj5bHkMwXJG3DDDLm1
|
||||
9aJDJfwjqEnCFT7SCggZFCBpu3PqEkq8jHGC/gnWcDoPhWtMldBRVb3MjsxjOi9t
|
||||
Lk39XcoQOgYo6aFMD1Ughbg+P2QrQwvLhtIl7134MUiB65IsDRLrjXkkMhVEe1Um
|
||||
0yL4doZPxZ/jm+dGxtFWcAXWBTL4lzE3fWCwMmygiuxljLl9n67glsZG7isRVMfY
|
||||
U9O8kAMRoMCiktnIe+Ecw1RmAmjgDmA/jTKPGuJRTj/WO5LtWwHUXa7jptJLU1tZ
|
||||
kdXx0SzOArmDG0dMwggSm9ms4Z8FT+XXe1BWKV1jLTDqBRP1z/KDmQENBF5izVcB
|
||||
CADKkNZwGmtcTR7TN1tuC326+oXNewWRraKdnxWiKXW1gUROBDiWPic9hImYYjky
|
||||
t6dz4DkAB/qJfAiRTZG/zz/qnTgbrzK9j3v4TlBTUcTtCI4fF/ShzutKpaIfWFDe
|
||||
lKSIoWRh/gY6LrtnXm+PRLTckzQxUP71HrHlFFk3462+Ph+7V3z5PrUZvbv+wJ3U
|
||||
5GdhhYEIBpq2fkvv2K9l9MFVWXcH7mDLxX7p/Q8OaHaSsdTtpBpky4IuA0RQiej0
|
||||
gXAEPuoO/TXKwtZ6G7eFjtcndomM2H3N7oYqZZuNW8lU+zcaV8HWKlwNZFvnkRAO
|
||||
05zCtN8ljUTWkZwM8k6BOp3bABEBAAG0H0phbmUgRG9lIDxqYW5lLmRvZUBleGFt
|
||||
cGxlLmNvbT6JAVQEEwEIAD4WIQQM1Ty6UL70MGRjStf3hCpc6qnAsQUCXmLNVwIb
|
||||
AwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD3hCpc6qnAsT7uCACe
|
||||
Ka0jKSzGmjVhxzTT8uO1bxZXzTLi2vDQcqFBVYUBe0TXgf9I8+0sWkvZfvZw9Mju
|
||||
3bxY+Tp1/e7+nKsEkQO/7rRureOa7OF/D5jJNX1QUNqUFF6LCPABP5RroHS3uGfd
|
||||
CHKyj84jrZPAhTDPMyYlrWkv8EX2YOT6dlnxgElIdc3WcwJSAtFTWQXoY/kHmjoU
|
||||
e8c8NJFN1nwEHzbKtjsnkcXvs7XruUhhmqsizyCyrIS9We4Sl5r+4zKi10FKoN/k
|
||||
zCjU3EHOFiQ8/l5rKTMM1lAN4q3Wyq2xeqyJ+UDx7hOGnnxKjeX9uLay5cAy7XhO
|
||||
wghIQKCXtcd3T/EzlYQNuQENBF5izVcBCACiADPuJIRFkIuMLiovrWCAtlXt3Oyy
|
||||
ZrchtRDzxLJW6nL6vaMoJ7nUabD6mlv9mfWRLG4exUID6632/mXblVcPYU4ZQM9H
|
||||
Futwi8cgq2SuiX/UJM0deJzmiQKxMNO4hUf7eQU7227jRdxkWaOjzN7xdayH6yld
|
||||
VyrPWQM8i4qmpsGPZ3/EYswDhxcPYPhkA1gW1tgaUxWf/k9U1+GYmyaAI71pImRE
|
||||
xUIc0pIv44IdGQRU3iPusgljDDXgPVhwF1EmAqFQ6aIM32h6x3WMT6u6OtWfGUks
|
||||
G3RBv3M380Tegppzk2X+2V38YaJH4u/jNUXhwbu/9yR4xuA02/Z1E44bABEBAAGJ
|
||||
ATwEGAEIACYWIQQM1Ty6UL70MGRjStf3hCpc6qnAsQUCXmLNVwIbDAUJA8JnAAAK
|
||||
CRD3hCpc6qnAsWb/CACKcrLYF4yK+vhdlTJw65znSBjIw16iH5SVyd+z3MmiQzlq
|
||||
pjxfMg7iJG1wDNl4wDRa3QipRrVhKvc8wRuUK7xUKUcbIJDBmoTcMj5iicPucr1W
|
||||
Qvv9OY3wXBBoWxdPgxWRBglbuZmp7s2D3ixbrd3nxGisFGXIkOAeWtOibFHnnaKw
|
||||
sM+xqpYDwn4PL/DYWuh0gT5RW6JG1PWdGWMr1CHjV8VyPD3dR59l976sRGz2oyu5
|
||||
gefZHtRecI65/BHtKtdf6ZZaonNXilK1XVrD2hahn0CKwumeQtS4kx/hB+SPrcCg
|
||||
CdUD7FFlMA3kGZ7rBKMkHVxoOondTAJFUw4TAE6j
|
||||
=DEtc
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
73
util/gpg/testdata/multi2.asc
vendored
Normal file
73
util/gpg/testdata/multi2.asc
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBF5izSABCADAED7eSbx+ol3TLt/fJ6UZciaItts4Rar83bj8LPTFZebWTHzy
|
||||
m0zoNdU3UrH3I8iWhoUUE1voqp2Hs3GEX3fHK70BodhGkGl5W931l8yYqTVlLYhE
|
||||
8MxwWZKwh3phK6Wcm9GUEA3BQr5rNApWwUfgCK8NHRl2Kmb5ujmPgoap2RsH6Fpn
|
||||
85gaCfUOvTV7jAZtY+LU84ZsVh0TcNoA4UieYHYWvXtYci9C0EkVbjpoRhZOkv5h
|
||||
oQSBm/5Kfv+d7kZUluBsm1yyXfdHJBVuNYd7SpHe6PO3+eQ/JgqlRSfs1UBKYgx3
|
||||
Sxapy16hm8vVAzE9vnxB87z0+kS0uc0Ri+abABEBAAG0H0pvaG4gRG9lIDxqb2hu
|
||||
LmRvZUBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQRG7spZs6IkAFi4SpP9x5gVQA2I
|
||||
qQUCXmLNIAIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9x5gV
|
||||
QA2IqS84CACXKj4xJ+UAkh5q/M0jXC9JxzQu8JtVE7cGTTFyjLBApGmtfa5RtEct
|
||||
QShhpdVhpuh2DhsySoza6acvwaP356HywFH64Q0MXo98XosEnSwab74k1yyd2QPR
|
||||
u+kIskEbfs/j6e5uYpqf0tCvXsxIywktGcdvLE/98ISXqHS8R1uCuMrfWR9Rrz/b
|
||||
8k4NY5u6IZCa+HmrZ7v3K4s1XaHbSJaz5MzJI2kFT6Ai485KBf7Iof5llr9x5U0L
|
||||
rEiH1u2xIh64WvqO6u6xqxas1ewzuI6tGECU2sllZxPIt6/onCZy9LnOjJOhEAyT
|
||||
P7N+q5jsF+NvqvCame9hmYDSfUv6TP9IuQENBF5izSABCACv6y8rmRC0otzl7A9p
|
||||
yfoNH9FNpLaiYuT6XUMSSC97TG1jjPZ3+6TP1Ff6nEwDxf57zRq8yJZO8LMRXwyA
|
||||
kIT6ZPB9lY4Z6qy1TZAd2/UVG6KR9kml+S/hOo2Y9WAz8tDpYM9rGieIW+LXcueK
|
||||
lkI0TYS7FX49UFB/hXJMnnOhzZxihVo/g1rlAPLsxE2i/1TVmDD0EOMwiuOBwoyN
|
||||
UurJq41sXsxYZQFAjCbUfuvWgXjM/ir97Rr8Vca5SjGNf9C4yLsDGl/eKfKPLUwP
|
||||
7cgnq/pSpVaWDEAb6DyU8ttY7zZQOQjT5Gwfggxzz9U4qUOOtFkQ6piQoe1Lyzi8
|
||||
6cHZABEBAAGJATwEGAEIACYWIQRG7spZs6IkAFi4SpP9x5gVQA2IqQUCXmLNIAIb
|
||||
DAUJA8JnAAAKCRD9x5gVQA2IqX09CACTALlaIOxa9VlBrhaj5bHkMwXJG3DDDLm1
|
||||
9aJDJfwjqEnCFT7SCggZFCBpu3PqEkq8jHGC/gnWcDoPhWtMldBRVb3MjsxjOi9t
|
||||
Lk39XcoQOgYo6aFMD1Ughbg+P2QrQwvLhtIl7134MUiB65IsDRLrjXkkMhVEe1Um
|
||||
0yL4doZPxZ/jm+dGxtFWcAXWBTL4lzE3fWCwMmygiuxljLl9n67glsZG7isRVMfY
|
||||
U9O8kAMRoMCiktnIe+Ecw1RmAmjgDmA/jTKPGuJRTj/WO5LtWwHUXa7jptJLU1tZ
|
||||
kdXx0SzOArmDG0dMwggSm9ms4Z8FT+XXe1BWKV1jLTDqBRP1z/KDmQENBF5izVcB
|
||||
CADKkNZwGmtcTR7TN1tuC326+oXNewWRraKdnxWiKXW1gUROBDiWPic9hImYYjky
|
||||
t6dz4DkAB/qJfAiRTZG/zz/qnTgbrzK9j3v4TlBTUcTtCI4fF/ShzutKpaIfWFDe
|
||||
lKSIoWRh/gY6LrtnXm+PRLTckzQxUP71HrHlFFk3462+Ph+7V3z5PrUZvbv+wJ3U
|
||||
5GdhhYEIBpq2fkvv2K9l9MFVWXcH7mDLxX7p/Q8OaHaSsdTtpBpky4IuA0RQiej0
|
||||
gXAEPuoO/TXKwtZ6G7eFjtcndomM2H3N7oYqZZuNW8lU+zcaV8HWKlwNZFvnkRAO
|
||||
05zCtN8ljUTWkZwM8k6BOp3bABEBAAG0H0phbmUgRG9lIDxqYW5lLmRvZUBleGFt
|
||||
cGxlLmNvbT6JAVQEEwEIAD4WIQQM1Ty6UL70MGRjStf3hCpc6qnAsQUCXmLNVwIb
|
||||
AwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD3hCpc6qnAsT7uCACe
|
||||
Ka0jKSzGmjVhxzTT8uO1bxZXzTLi2vDQcqFBVYUBe0TXgf9I8+0sWkvZfvZw9Mju
|
||||
3bxY+Tp1/e7+nKsEkQO/7rRureOa7OF/D5jJNX1QUNqUFF6LCPABP5RroHS3uGfd
|
||||
CHKyj84jrZPAhTDPMyYlrWkv8EX2YOT6dlnxgElIdc3WcwJSAtFTWQXoY/kHmjoU
|
||||
e8c8NJFN1nwEHzbKtjsnkcXvs7XruUhhmqsizyCyrIS9We4Sl5r+4zKi10FKoN/k
|
||||
zCjU3EHOFiQ8/l5rKTMM1lAN4q3Wyq2xeqyJ+UDx7hOGnnxKjeX9uLay5cAy7XhO
|
||||
wghIQKCXtcd3T/EzlYQNuQENBF5izVcBCACiADPuJIRFkIuMLiovrWCAtlXt3Oyy
|
||||
ZrchtRDzxLJW6nL6vaMoJ7nUabD6mlv9mfWRLG4exUID6632/mXblVcPYU4ZQM9H
|
||||
Futwi8cgq2SuiX/UJM0deJzmiQKxMNO4hUf7eQU7227jRdxkWaOjzN7xdayH6yld
|
||||
VyrPWQM8i4qmpsGPZ3/EYswDhxcPYPhkA1gW1tgaUxWf/k9U1+GYmyaAI71pImRE
|
||||
xUIc0pIv44IdGQRU3iPusgljDDXgPVhwF1EmAqFQ6aIM32h6x3WMT6u6OtWfGUks
|
||||
G3RBv3M380Tegppzk2X+2V38YaJH4u/jNUXhwbu/9yR4xuA02/Z1E44bABEBAAGJ
|
||||
ATwEGAEIACYWIQQM1Ty6UL70MGRjStf3hCpc6qnAsQUCXmLNVwIbDAUJA8JnAAAK
|
||||
CRD3hCpc6qnAsWb/CACKcrLYF4yK+vhdlTJw65znSBjIw16iH5SVyd+z3MmiQzlq
|
||||
pjxfMg7iJG1wDNl4wDRa3QipRrVhKvc8wRuUK7xUKUcbIJDBmoTcMj5iicPucr1W
|
||||
Qvv9OY3wXBBoWxdPgxWRBglbuZmp7s2D3ixbrd3nxGisFGXIkOAeWtOibFHnnaKw
|
||||
sM+xqpYDwn4PL/DYWuh0gT5RW6JG1PWdGWMr1CHjV8VyPD3dR59l976sRGz2oyu5
|
||||
gefZHtRecI65/BHtKtdf6ZZaonNXilK1XVrD2hahn0CKwumeQtS4kx/hB+SPrcCg
|
||||
CdUD7FFlMA3kGZ7rBKMkHVxoOondTAJFUw4TAE6j
|
||||
=DEtc
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
-----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-----
|
||||
3
util/gpg/testdata/unknown_signature.txt
vendored
Normal file
3
util/gpg/testdata/unknown_signature.txt
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
gpg: Signature made Mon Aug 26 20:59:48 2019 CEST
|
||||
gpg: using RSA key 4AEE18F83AFDEB23
|
||||
gpg: Can't check signature: No public key
|
||||
|
|
@ -891,6 +891,27 @@ func (mgr *SettingsManager) SaveTLSCertificateData(ctx context.Context, tlsCerti
|
|||
return mgr.ResyncInformers()
|
||||
}
|
||||
|
||||
func (mgr *SettingsManager) SaveGPGPublicKeyData(ctx context.Context, gpgPublicKeys map[string]string) error {
|
||||
err := mgr.ensureSynced(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keysCM, err := mgr.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keysCM.Data = gpgPublicKeys
|
||||
_, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Update(keysCM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mgr.ResyncInformers()
|
||||
|
||||
}
|
||||
|
||||
// NewSettingsManager generates a new SettingsManager pointer and returns it
|
||||
func NewSettingsManager(ctx context.Context, clientset kubernetes.Interface, namespace string) *SettingsManager {
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue