Compare commits

...

8 Commits

Author SHA1 Message Date
Jesse Suen
13558b7ce8 Revert change to redact credentials since logic is reused by controller 2018-05-03 16:42:21 -07:00
Alexander Matyushentsev
3b2b3dacf5 Update version 2018-05-03 15:58:09 -07:00
Alexander Matyushentsev
1b2f89995c Issue #155 - Application update failes due to concurrent access (#156) 2018-05-03 15:55:01 -07:00
Jesse Suen
0479fcdf82 Add settings endpoint so frontend can show/hide SSO login button. Rename config to settings (#153) 2018-05-03 11:18:44 -07:00
Andrew Merenbach
a04465466d Add workflow for blue-green deployments (#148)
* Add prototype script and temp README

* Clean up code and support command-line args

* Flesh out logic more now

* Start workflow

* Update Blue-Green workflow

* Rm original example script

* Update comments

* Add argo parameters; use compact output for jq

* Fix some missing values, use workflow parameters

* Separate out necessary parameters/outputs

* Get bluegreen workflow working

* Mv bluegreen.yaml to workflows/, thanks @jessesuen

* Rm TODO statements
2018-05-03 09:52:46 -07:00
Jesse Suen
670921df90 SSO Support (#152)
This change implements SSO support.

dex is run as a sidecar to the ArgoCD API server, which fronts dex using a reverse proxy. The end result is that the ArgoCD acts as an OIDC provider serving under /api/dex. The login flow begins at /auth/login, which redirects to the Dex's OAuth2 consent page and ultimately directed to the IdP provider's login page, where they enter their credentials. After logging in, the OAuth2 redirect flows back to the client app, ultimately reaching /auth/callback, where the OIDC token claims are signed, and persisted in the users's cookie.

The dex configuration YAML is formulated during startup (through the argocd-util utility), with the configuration values taken from the argocd-cm configmap and the argocd-secret.

The build process was refactored to build argocd-util statically, so that it could be run inside off-the-shelf dex, which is built from alpine. Also, build speed was improved by expanding the default make targets in the Dockerfile, to avoid rebuilding each binary from scratch

Session management was refactored to use more bare-bones jwt library constructs, so we could reuse code from the user/password flow vs. OAuth2 flow.

* Initial SSO support. Run dex as sidecar. Generate dex config from ArgoCD cm and secret
* Sign and write SSO claims to JWT cookie during SSO login. Refactor session manager
* Build argo-util statically so it can run in dex sidecar. Redirect after SSO login
* Simplify app creation process to not require communication to dex gRPC server
2018-05-02 22:02:26 -07:00
Edward Lee
18f7e17d7a Added OWNERS file 2018-05-02 18:24:50 -07:00
Andrew Merenbach
a2aede0441 Redact sensitive repo/cluster information upon retrieval (#150)
* Redact sensitive cluster information upon retrieval

* Redact git username for now, too

* Revert "Redact git username for now, too"

This reverts commit d9e2eba37e.
2018-05-01 16:58:03 -07:00
36 changed files with 2882 additions and 426 deletions

View File

@@ -40,9 +40,9 @@ RUN cd ${GOPATH}/src/dummy && \
rmdir vendor
# Perform the build
ARG MAKE_TARGET
WORKDIR /root/go/src/github.com/argoproj/argo-cd
COPY . .
ARG MAKE_TARGET="cli server controller repo-server argocd-util"
RUN make ${MAKE_TARGET}
@@ -72,13 +72,12 @@ FROM debian:9.3
RUN apt-get update && apt-get install -y git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
ARG BINARY
COPY --from=builder /root/go/src/github.com/argoproj/argo-cd/dist/${BINARY} /${BINARY}
COPY --from=cli-tooling /ks /usr/local/bin/ks
COPY --from=cli-tooling /kubectl /usr/local/bin/kubectl
# workaround ksonnet issue https://github.com/ksonnet/ksonnet/issues/298
ENV USER=root
ENV BINARY=$BINARY
CMD /$BINARY
COPY --from=builder /root/go/src/github.com/argoproj/argo-cd/dist/* /
ARG BINARY
CMD /${BINARY}

35
Gopkg.lock generated
View File

@@ -32,6 +32,18 @@
revision = "2ee87856327ba09384cabd113bc6b5d174e9ec0f"
version = "v3.5.1"
[[projects]]
name = "github.com/coreos/dex"
packages = ["api"]
revision = "218d671a96865df2a4cf7f310efb99b8bfc5a5e2"
version = "v2.10.0"
[[projects]]
branch = "v2"
name = "github.com/coreos/go-oidc"
packages = ["."]
revision = "1180514eaf4d9f38d0d19eef639a1d695e066e72"
[[projects]]
branch = "master"
name = "github.com/daaku/go.zipexe"
@@ -328,6 +340,15 @@
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/pquerna/cachecontrol"
packages = [
".",
"cacheobject"
]
revision = "525d0eb5f91d30e3b1548de401b7ef9ea6898520"
[[projects]]
name = "github.com/sergi/go-diff"
packages = ["diffmatchpatch"]
@@ -402,6 +423,8 @@
packages = [
"bcrypt",
"blowfish",
"ed25519",
"ed25519/internal/edwards25519",
"ssh/terminal"
]
revision = "432090b8f568c018896cd8a0fb0345872bbac6ce"
@@ -542,6 +565,16 @@
revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4"
version = "v0.9.0"
[[projects]]
name = "gopkg.in/square/go-jose.v2"
packages = [
".",
"cipher",
"json"
]
revision = "76dd09796242edb5b897103a75df2645c028c960"
version = "v2.1.6"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
@@ -765,6 +798,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "8e7142f84554c6f1665ef18e0fb906f82de8cd802b0211c4a46ec1ad228b8b7e"
inputs-digest = "562f3dc64b35c3db83ab1e05f0aba9bbe52d5dfda905394b88ca22b8746a2cc5"
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -41,7 +41,7 @@ IMAGE_PREFIX=${IMAGE_NAMESPACE}/
endif
.PHONY: all
all: cli server-image controller-image repo-server-image
all: cli server-image controller-image repo-server-image argocd-util
.PHONY: protogen
protogen:
@@ -74,13 +74,17 @@ cli-darwin:
docker cp tmp-argocd-darwin:/root/go/src/github.com/argoproj/argo-cd/dist/argocd-darwin-amd64 dist/
docker rm tmp-argocd-darwin
.PHONY: argocd-util
argocd-util:
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS} -extldflags "-static"' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
.PHONY: server
server:
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
.PHONY: server-image
server-image:
docker build --build-arg BINARY=argocd-server --build-arg MAKE_TARGET=server -t $(IMAGE_PREFIX)argocd-server:$(IMAGE_TAG) -f Dockerfile-argocd .
docker build --build-arg BINARY=argocd-server -t $(IMAGE_PREFIX)argocd-server:$(IMAGE_TAG) -f Dockerfile-argocd .
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-server:$(IMAGE_TAG) ; fi
.PHONY: repo-server
@@ -89,7 +93,7 @@ repo-server:
.PHONY: repo-server-image
repo-server-image:
docker build --build-arg BINARY=argocd-repo-server --build-arg MAKE_TARGET=repo-server -t $(IMAGE_PREFIX)argocd-repo-server:$(IMAGE_TAG) -f Dockerfile-argocd .
docker build --build-arg BINARY=argocd-repo-server -t $(IMAGE_PREFIX)argocd-repo-server:$(IMAGE_TAG) -f Dockerfile-argocd .
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-repo-server:$(IMAGE_TAG) ; fi
.PHONY: controller
@@ -98,12 +102,12 @@ controller:
.PHONY: controller-image
controller-image:
docker build --build-arg BINARY=argocd-application-controller --build-arg MAKE_TARGET=controller -t $(IMAGE_PREFIX)argocd-application-controller:$(IMAGE_TAG) -f Dockerfile-argocd .
docker build --build-arg BINARY=argocd-application-controller -t $(IMAGE_PREFIX)argocd-application-controller:$(IMAGE_TAG) -f Dockerfile-argocd .
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-application-controller:$(IMAGE_TAG) ; fi
.PHONY: cli-image
cli-image:
docker build --build-arg BINARY=argocd --build-arg MAKE_TARGET=cli -t $(IMAGE_PREFIX)argocd-cli:$(IMAGE_TAG) -f Dockerfile-argocd .
docker build --build-arg BINARY=argocd -t $(IMAGE_PREFIX)argocd-cli:$(IMAGE_TAG) -f Dockerfile-argocd .
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-cli:$(IMAGE_TAG) ; fi
.PHONY: builder-image

8
OWNERS Normal file
View File

@@ -0,0 +1,8 @@
owners:
- alexmt
- jessesuen
approvers:
- alexmt
- jessesuen
- merenbach

View File

@@ -1,3 +1,4 @@
controller: go run ./cmd/argocd-application-controller/main.go --app-resync 10
api-server: go run ./cmd/argocd-server/main.go --insecure
api-server: go run ./cmd/argocd-server/main.go --insecure --disable-auth
repo-server: go run ./cmd/argocd-repo-server/main.go
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p 5556:5556 -p 5557:5557 -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/coreos/dex:v2.10.0 serve /dex.yaml"

View File

@@ -1 +1 @@
0.4.0
0.3.3

127
cmd/argocd-util/main.go Normal file
View File

@@ -0,0 +1,127 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"syscall"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/dex"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
// load the gcp plugin (required to authenticate against GKE clusters).
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
// load the oidc plugin (required to authenticate with OpenID Connect).
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
)
const (
// CLIName is the name of the CLI
cliName = "argocd-util"
)
// NewCommand returns a new instance of an argocd command
func NewCommand() *cobra.Command {
var (
logLevel string
)
var command = &cobra.Command{
Use: cliName,
Short: "argocd-util has internal tools used by ArgoCD",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
},
}
command.AddCommand(cli.NewVersionCmd(cliName))
command.AddCommand(NewRunDexCommand())
command.AddCommand(NewGenDexConfigCommand())
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
return command
}
func NewRunDexCommand() *cobra.Command {
var (
clientConfig clientcmd.ClientConfig
)
var command = cobra.Command{
Use: "rundex",
Short: "Runs dex generating a config using settings from the ArgoCD configmap and secret",
RunE: func(c *cobra.Command, args []string) error {
dexPath, err := exec.LookPath("dex")
errors.CheckError(err)
dexCfgBytes, err := genDexConfig(clientConfig)
errors.CheckError(err)
if len(dexCfgBytes) == 0 {
log.Infof("dex is not configured")
// need to sleep forever since we run as a sidecar and kubernetes does not permit
// containers in a deployment to have restartPolicy anything other than Always.
// TODO: we should watch for a change in the dex.config key in the config-map
// to restart dex when there is a change (e.g. clientID and clientSecretKey changed)
select {}
}
err = ioutil.WriteFile("/tmp/dex.yaml", dexCfgBytes, 0644)
errors.CheckError(err)
log.Info(string(dexCfgBytes))
return syscall.Exec(dexPath, []string{"dex", "serve", "/tmp/dex.yaml"}, []string{})
},
}
clientConfig = cli.AddKubectlFlagsToCmd(&command)
return &command
}
func NewGenDexConfigCommand() *cobra.Command {
var (
clientConfig clientcmd.ClientConfig
out string
)
var command = cobra.Command{
Use: "gendexcfg",
Short: "Generates a dex config from ArgoCD settings",
RunE: func(c *cobra.Command, args []string) error {
dexCfgBytes, err := genDexConfig(clientConfig)
errors.CheckError(err)
if len(dexCfgBytes) == 0 {
log.Infof("dex is not configured")
return nil
}
if out == "" {
fmt.Printf(string(dexCfgBytes))
} else {
err = ioutil.WriteFile(out, dexCfgBytes, 0644)
errors.CheckError(err)
}
return nil
},
}
clientConfig = cli.AddKubectlFlagsToCmd(&command)
command.Flags().StringVarP(&out, "out", "o", "", "Output to the specified file instead of stdout")
return &command
}
func genDexConfig(clientConfig clientcmd.ClientConfig) ([]byte, error) {
config, err := clientConfig.ClientConfig()
errors.CheckError(err)
namespace, _, err := clientConfig.Namespace()
errors.CheckError(err)
kubeClient := kubernetes.NewForConfigOrDie(config)
return dex.GenerateDexConfigYAML(kubeClient, namespace)
}
func main() {
if err := NewCommand().Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

@@ -217,7 +217,10 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
os.Exit(1)
}
setParameterOverrides(app, appOpts.parameters)
_, err = appIf.Update(context.Background(), app)
_, err = appIf.UpdateSpec(context.Background(), &application.ApplicationSpecRequest{
AppName: app.Name,
Spec: &app.Spec,
})
errors.CheckError(err)
},
}

View File

@@ -14,6 +14,9 @@ const (
// SecretTypeCluster indicates a secret type of cluster
SecretTypeCluster = "cluster"
// AuthCookieName is the HTTP cookie name where we store our auth token
AuthCookieName = "argocd.argoproj.io/auth-token"
)
const (

View File

@@ -25,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
@@ -222,26 +223,26 @@ func (ctrl *ApplicationController) processNextItem() bool {
if isForceRefreshed || app.NeedRefreshAppStatus(ctrl.statusRefreshTimeout) {
log.Infof("Refreshing application '%s' status (force refreshed: %v)", app.Name, isForceRefreshed)
status, err := ctrl.tryRefreshAppStatus(app.DeepCopy())
comparisonResult, parameters, err := ctrl.tryRefreshAppStatus(app.DeepCopy())
if err != nil {
status = app.Status.DeepCopy()
status.ComparisonResult = appv1.ComparisonResult{
comparisonResult = &appv1.ComparisonResult{
Status: appv1.ComparisonStatusError,
Error: fmt.Sprintf("Failed to get application status for application '%s': %v", app.Name, err),
ComparedTo: app.Spec.Source,
ComparedAt: metav1.Time{Time: time.Now().UTC()},
}
parameters = nil
}
ctrl.updateAppStatus(app.Name, app.Namespace, status)
ctrl.updateAppStatus(app.Name, app.Namespace, comparisonResult, parameters)
}
return true
}
func (ctrl *ApplicationController) tryRefreshAppStatus(app *appv1.Application) (*appv1.ApplicationStatus, error) {
func (ctrl *ApplicationController) tryRefreshAppStatus(app *appv1.Application) (*appv1.ComparisonResult, *[]appv1.ComponentParameter, error) {
conn, client, err := ctrl.repoClientset.NewRepositoryClient()
if err != nil {
return nil, err
return nil, nil, err
}
defer util.Close(conn)
repo, err := ctrl.apiRepoService.Get(context.Background(), &apireposerver.RepoQuery{Repo: app.Spec.Source.RepoURL})
@@ -271,14 +272,14 @@ func (ctrl *ApplicationController) tryRefreshAppStatus(app *appv1.Application) (
})
if err != nil {
log.Errorf("Failed to load application manifest %v", err)
return nil, err
return nil, nil, err
}
targetObjs := make([]*unstructured.Unstructured, len(manifestInfo.Manifests))
for i, manifestStr := range manifestInfo.Manifests {
var obj unstructured.Unstructured
if err := json.Unmarshal([]byte(manifestStr), &obj); err != nil {
if err != nil {
return nil, err
return nil, nil, err
}
}
targetObjs[i] = &obj
@@ -287,11 +288,10 @@ func (ctrl *ApplicationController) tryRefreshAppStatus(app *appv1.Application) (
server, namespace := argoutil.ResolveServerNamespace(app.Spec.Destination, manifestInfo)
comparisonResult, err := ctrl.appComparator.CompareAppState(server, namespace, targetObjs, app)
if err != nil {
return nil, err
return nil, nil, err
}
log.Infof("App %s comparison result: prev: %s. current: %s", app.Name, app.Status.ComparisonResult.Status, comparisonResult.Status)
newStatus := app.Status
newStatus.ComparisonResult = *comparisonResult
paramsReq := repository.EnvParamsRequest{
Repo: repo,
Revision: revision,
@@ -300,13 +300,13 @@ func (ctrl *ApplicationController) tryRefreshAppStatus(app *appv1.Application) (
}
params, err := client.GetEnvParams(context.Background(), &paramsReq)
if err != nil {
return nil, err
return nil, nil, err
}
newStatus.Parameters = make([]appv1.ComponentParameter, len(params.Params))
parameters := make([]appv1.ComponentParameter, len(params.Params))
for i := range params.Params {
newStatus.Parameters[i] = *params.Params[i]
parameters[i] = *params.Params[i]
}
return &newStatus, nil
return comparisonResult, &parameters, nil
}
func (ctrl *ApplicationController) runWorker() {
@@ -314,23 +314,22 @@ func (ctrl *ApplicationController) runWorker() {
}
}
func (ctrl *ApplicationController) updateAppStatus(appName string, namespace string, status *appv1.ApplicationStatus) {
appKey := fmt.Sprintf("%s/%s", namespace, appName)
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(appKey)
func (ctrl *ApplicationController) updateAppStatus(appName string, namespace string, comparisonResult *appv1.ComparisonResult, parameters *[]appv1.ComponentParameter) {
statusPatch := make(map[string]interface{})
statusPatch["comparisonResult"] = comparisonResult
statusPatch["parameters"] = parameters
patch, err := json.Marshal(map[string]interface{}{
"status": statusPatch,
})
if err == nil {
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(namespace)
_, err = appClient.Patch(appName, types.MergePatchType, patch)
}
if err != nil {
log.Warnf("Failed to get application '%s' from informer index: %+v", appKey, err)
log.Warnf("Error updating application: %v", err)
} else {
if exists {
app := obj.(*appv1.Application).DeepCopy()
app.Status = *status
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(namespace)
_, err := appClient.Update(app)
if err != nil {
log.Warnf("Error updating application: %v", err)
} else {
log.Info("Application update successful")
}
}
log.Info("Application update successful")
}
}

View File

@@ -7,11 +7,11 @@ import (
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/util/config"
"github.com/argoproj/argo-cd/util/diff"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/password"
"github.com/argoproj/argo-cd/util/session"
"github.com/argoproj/argo-cd/util/settings"
tlsutil "github.com/argoproj/argo-cd/util/tls"
"github.com/ghodss/yaml"
"github.com/gobuffalo/packr"
@@ -131,8 +131,8 @@ func (i *Installer) InstallApplicationCRD() {
func (i *Installer) InstallSettings() {
kubeclientset, err := kubernetes.NewForConfig(i.config)
errors.CheckError(err)
configManager := config.NewConfigManager(kubeclientset, i.Namespace)
_, err = configManager.GetSettings()
settingsMgr := settings.NewSettingsManager(kubeclientset, i.Namespace)
_, err = settingsMgr.GetSettings()
if err == nil {
log.Infof("Settings already exists. Skipping creation")
return
@@ -141,7 +141,7 @@ func (i *Installer) InstallSettings() {
log.Fatal(err)
}
// configmap/secret not yet created
var newSettings config.ArgoCDSettings
var newSettings settings.ArgoCDSettings
// set JWT signature
signature, err := session.MakeSignature(32)
@@ -173,7 +173,7 @@ func (i *Installer) InstallSettings() {
errors.CheckError(err)
newSettings.Certificate = cert
err = configManager.SaveSettings(&newSettings)
err = settingsMgr.SaveSettings(&newSettings)
errors.CheckError(err)
}
@@ -222,8 +222,10 @@ func (i *Installer) InstallArgoCDServer() {
i.unmarshalManifest("04c_argocd-server-rolebinding.yaml", &argoCDServerControllerRoleBinding)
i.unmarshalManifest("04d_argocd-server-deployment.yaml", &argoCDServerControllerDeployment)
i.unmarshalManifest("04e_argocd-server-service.yaml", &argoCDServerService)
argoCDServerControllerDeployment.Spec.Template.Spec.InitContainers[0].Image = i.UIImage
argoCDServerControllerDeployment.Spec.Template.Spec.InitContainers[0].Image = i.ServerImage
argoCDServerControllerDeployment.Spec.Template.Spec.InitContainers[0].ImagePullPolicy = apiv1.PullPolicy(i.ImagePullPolicy)
argoCDServerControllerDeployment.Spec.Template.Spec.InitContainers[1].Image = i.UIImage
argoCDServerControllerDeployment.Spec.Template.Spec.InitContainers[1].ImagePullPolicy = apiv1.PullPolicy(i.ImagePullPolicy)
argoCDServerControllerDeployment.Spec.Template.Spec.Containers[0].Image = i.ServerImage
argoCDServerControllerDeployment.Spec.Template.Spec.Containers[0].ImagePullPolicy = apiv1.PullPolicy(i.ImagePullPolicy)
i.MustInstallResource(kube.MustToUnstructured(&argoCDServerServiceAccount))

View File

@@ -1,7 +1,57 @@
# NOTE: the values here are just a example and are not the values used during an install.
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
# TODO: future argocd tuning keys go here (e.g. resync period)
data: {}
data:
# url is the externally facing base URL of ArgoCD.
# This field is required when configuring SSO, which ArgoCD uses as part the redirectURI for the
# dex connectors. When configuring the application in the SSO provider (e.g. github, okta), the
# authorization callback URL will be url + /api/dex/callback. For example, if ArgoCD's url is
# https://example.com, then the auth callback to set in the SSO provider should be:
# https://example.com/api/dex/callback
url: http://localhost:8080
# dex.config holds the contents of the configuration yaml for the dex OIDC/Oauth2 provider sidecar.
# Only a subset of a full dex config is required, namely the connectors list. ArgoCD will generate
# the complete dex config based on the configured URL, and the known callback endpoint which the
# ArgoCD API server exposes (i.e. /api/dex/callback).
dex.config: |
# connectors is a list of dex connector configurations. For details on available connectors and
# how to configure them, see: https://github.com/coreos/dex/tree/master/Documentation/connectors
# NOTE:
# * Any values which start with '$' will look to a key in argocd-secret of the same name, to
# obtain the actual value.
# * ArgoCD will automatically set the 'redirectURI' field in any OAuth2 connectors, to match the
# external callback URL (e.g. https://example.com/api/dex/callback)
connectors:
# GitHub example
- type: github
id: github
name: GitHub
config:
clientID: $github.clientID
clientSecret: $github.clientSecret
orgs:
- name: your-github-org
# GitHub enterprise example
- type: github
id: acme-github
name: Acme GitHub
config:
hostName: github.acme.com
clientID: $acme.clientID
clientSecret: $acme.clientSecret
orgs:
- name: your-github-org
# OIDC example (e.g. Okta)
- type: oidc
id: okta
name: Okta
config:
issuer: https://dev-123456.oktapreview.com
clientID: $okta.clientID
clientSecret: $okta.clientSecret

View File

@@ -6,8 +6,19 @@ metadata:
name: argocd-secret
namespace: argocd
type: Opaque
data:
# bcrypt hash of 'password'
admin.password: JDJhJDEwJGVYYkZmOEt3NUMzTDJVbE9FRDNqUU9QMC5reVNBamVLUXY0N3NqaFFpWlZwTkkyU2dMTzd1
stringData:
# bcrypt hash of the string "password"
admin.password: $2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W
# random server signature key for session validation
server.secretkey: aEDvv73vv70F77+9CRBSNu+/vTYQ77+9EUFh77+9LzFyJ++/vXfLsO+/vWRbeu+/ve+/vQ==
# the following of user defined keys which are referenced in the example argocd-cm configmap
# as pat of SSO configuration.
github.clientID: aabbccddeeff00112233
github.clientSecret: nv1vx8w4gw5byrflujfkxww6ykh85yq818aorvwy
acme.clientID: abcdefghijklmnopqrst
acme.clientSecret: 5pp7dyre3d5nyk0ree1tr0gd68k18xn94x8lfae9
okta.clientID: aaaabbbbccccddddeee
okta.clientSecret: x41ztv6ufyf07oyoopc6f62p222c00mox2ciquvt

View File

@@ -14,16 +14,28 @@ spec:
spec:
serviceAccountName: argocd-server
initContainers:
- command: [cp, -r, /app, /shared]
- name: copyutil
image: argoproj/argocd-server:latest
command: [cp, /argocd-util, /shared]
volumeMounts:
- mountPath: /shared
name: static-files
- name: ui
image: argoproj/argocd-ui:latest
name: argocd-server-ui
command: [cp, -r, /app, /shared]
volumeMounts:
- mountPath: /shared
name: static-files
containers:
- command: [/argocd-server, --staticassets, /shared/app, --repo-server, 'argocd-repo-server:8081']
- name: argocd-server
image: argoproj/argocd-server:latest
name: argocd-server
command: [/argocd-server, --staticassets, /shared/app, --repo-server, 'argocd-repo-server:8081']
volumeMounts:
- mountPath: /shared
name: static-files
- name: dex
image: quay.io/coreos/dex:v2.10.0
command: [/shared/argocd-util, rundex]
volumeMounts:
- mountPath: /shared
name: static-files

View File

@@ -24,6 +24,7 @@ import (
)
const (
MetaDataTokenKey = "token"
// EnvArgoCDServer is the environment variable to look for an ArgoCD server address
EnvArgoCDServer = "ARGOCD_SERVER"
// EnvArgoCDAuthToken is the environment variable to look for an ArgoCD auth token
@@ -138,7 +139,8 @@ func NewClientOrDie(opts *ClientOptions) ServerClient {
return client
}
// JwtCredentials holds a token for authentication.
// JwtCredentials implements the gRPC credentials.Credentials interface which we is used to do
// grpc.WithPerRPCCredentials(), for authentication
type jwtCredentials struct {
Token string
}
@@ -149,7 +151,8 @@ func (c jwtCredentials) RequireTransportSecurity() bool {
func (c jwtCredentials) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
return map[string]string{
"tokens": c.Token,
MetaDataTokenKey: c.Token,
"tokens": c.Token, // legacy key. delete eventually
}, nil
}

View File

@@ -75,7 +75,7 @@ func (s *Server) List(ctx context.Context, q *ApplicationQuery) (*appv1.Applicat
// Create creates an application
func (s *Server) Create(ctx context.Context, a *appv1.Application) (*appv1.Application, error) {
err := s.validateApp(ctx, a)
err := s.validateApp(ctx, &a.Spec)
if err != nil {
return nil, err
}
@@ -89,13 +89,29 @@ func (s *Server) Get(ctx context.Context, q *ApplicationQuery) (*appv1.Applicati
// Update updates an application
func (s *Server) Update(ctx context.Context, a *appv1.Application) (*appv1.Application, error) {
err := s.validateApp(ctx, a)
err := s.validateApp(ctx, &a.Spec)
if err != nil {
return nil, err
}
return s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Update(a)
}
// UpdateSpec updates an application spec
func (s *Server) UpdateSpec(ctx context.Context, q *ApplicationSpecRequest) (*appv1.ApplicationSpec, error) {
err := s.validateApp(ctx, q.Spec)
if err != nil {
return nil, err
}
patch, err := json.Marshal(map[string]appv1.ApplicationSpec{
"spec": *q.Spec,
})
if err != nil {
return nil, err
}
_, err = s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Patch(q.AppName, types.MergePatchType, patch)
return q.Spec, err
}
// Delete removes an application and all associated resources
func (s *Server) Delete(ctx context.Context, q *DeleteApplicationRequest) (*ApplicationResponse, error) {
var err error
@@ -164,20 +180,20 @@ func (s *Server) Watch(q *ApplicationQuery, ws ApplicationService_WatchServer) e
// * the git path contains a valid app.yaml
// * the specified environment exists
// * the referenced cluster has been added to ArgoCD
func (s *Server) validateApp(ctx context.Context, a *appv1.Application) error {
func (s *Server) validateApp(ctx context.Context, spec *appv1.ApplicationSpec) error {
// Test the repo
conn, repoClient, err := s.repoClientset.NewRepositoryClient()
if err != nil {
return err
}
defer util.Close(conn)
repoRes, err := s.repoService.Get(ctx, &apirepository.RepoQuery{Repo: a.Spec.Source.RepoURL})
repoRes, err := s.repoService.Get(ctx, &apirepository.RepoQuery{Repo: spec.Source.RepoURL})
if err != nil {
if errStatus, ok := status.FromError(err); ok && errStatus.Code() == codes.NotFound {
// The repo has not been added to ArgoCD so we do not have credentials to access it.
// We support the mode where apps can be created from public repositories. Test the
// repo to make sure it is publically accessible
err = git.TestRepo(a.Spec.Source.RepoURL, "", "", "")
// repo to make sure it is publicly accessible
err = git.TestRepo(spec.Source.RepoURL, "", "", "")
if err != nil {
return err
}
@@ -189,10 +205,10 @@ func (s *Server) validateApp(ctx context.Context, a *appv1.Application) error {
// Verify app.yaml is functional
req := repository.KsonnetAppRequest{
Repo: &appv1.Repository{
Repo: a.Spec.Source.RepoURL,
Repo: spec.Source.RepoURL,
},
Revision: a.Spec.Source.TargetRevision,
Path: a.Spec.Source.Path,
Revision: spec.Source.TargetRevision,
Path: spec.Source.Path,
}
if repoRes != nil {
req.Repo.Username = repoRes.Username
@@ -205,15 +221,15 @@ func (s *Server) validateApp(ctx context.Context, a *appv1.Application) error {
}
// Verify the specified environment is defined in it
envSpec, ok := ksAppRes.Environments[a.Spec.Source.Environment]
envSpec, ok := ksAppRes.Environments[spec.Source.Environment]
if !ok {
return status.Errorf(codes.InvalidArgument, "environment '%s' does not exist in app", a.Spec.Source.Environment)
return status.Errorf(codes.InvalidArgument, "environment '%s' does not exist in app", spec.Source.Environment)
}
// Ensure the k8s cluster the app is referencing, is configured in ArgoCD
// NOTE: need to check if it was overridden in the destination spec
clusterURL := envSpec.Destination.Server
if a.Spec.Destination != nil && a.Spec.Destination.Server != "" {
clusterURL = a.Spec.Destination.Server
if spec.Destination != nil && spec.Destination.Server != "" {
clusterURL = spec.Destination.Server
}
_, err = s.clusterService.Get(ctx, &cluster.ClusterQuery{Server: clusterURL})
if err != nil {

View File

@@ -16,6 +16,7 @@
ApplicationResponse
DeleteApplicationRequest
ApplicationSyncRequest
ApplicationSpecRequest
ApplicationSyncResult
ApplicationRollbackRequest
ResourceDetails
@@ -160,6 +161,33 @@ func (m *ApplicationSyncRequest) GetPrune() bool {
return false
}
// ApplicationSpecRequest is a request to update application spec
type ApplicationSpecRequest struct {
AppName string `protobuf:"bytes,1,opt,name=appName,proto3" json:"appName,omitempty"`
Spec *github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.ApplicationSpec `protobuf:"bytes,2,opt,name=spec" json:"spec,omitempty"`
}
func (m *ApplicationSpecRequest) Reset() { *m = ApplicationSpecRequest{} }
func (m *ApplicationSpecRequest) String() string { return proto.CompactTextString(m) }
func (*ApplicationSpecRequest) ProtoMessage() {}
func (*ApplicationSpecRequest) Descriptor() ([]byte, []int) {
return fileDescriptorApplication, []int{4}
}
func (m *ApplicationSpecRequest) GetAppName() string {
if m != nil {
return m.AppName
}
return ""
}
func (m *ApplicationSpecRequest) GetSpec() *github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.ApplicationSpec {
if m != nil {
return m.Spec
}
return nil
}
// ApplicationSyncResult is a result of a sync requeswt
type ApplicationSyncResult struct {
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
@@ -169,7 +197,7 @@ type ApplicationSyncResult struct {
func (m *ApplicationSyncResult) Reset() { *m = ApplicationSyncResult{} }
func (m *ApplicationSyncResult) String() string { return proto.CompactTextString(m) }
func (*ApplicationSyncResult) ProtoMessage() {}
func (*ApplicationSyncResult) Descriptor() ([]byte, []int) { return fileDescriptorApplication, []int{4} }
func (*ApplicationSyncResult) Descriptor() ([]byte, []int) { return fileDescriptorApplication, []int{5} }
func (m *ApplicationSyncResult) GetMessage() string {
if m != nil {
@@ -196,7 +224,7 @@ func (m *ApplicationRollbackRequest) Reset() { *m = ApplicationRollbackR
func (m *ApplicationRollbackRequest) String() string { return proto.CompactTextString(m) }
func (*ApplicationRollbackRequest) ProtoMessage() {}
func (*ApplicationRollbackRequest) Descriptor() ([]byte, []int) {
return fileDescriptorApplication, []int{5}
return fileDescriptorApplication, []int{6}
}
func (m *ApplicationRollbackRequest) GetName() string {
@@ -237,7 +265,7 @@ type ResourceDetails struct {
func (m *ResourceDetails) Reset() { *m = ResourceDetails{} }
func (m *ResourceDetails) String() string { return proto.CompactTextString(m) }
func (*ResourceDetails) ProtoMessage() {}
func (*ResourceDetails) Descriptor() ([]byte, []int) { return fileDescriptorApplication, []int{6} }
func (*ResourceDetails) Descriptor() ([]byte, []int) { return fileDescriptorApplication, []int{7} }
func (m *ResourceDetails) GetName() string {
if m != nil {
@@ -275,7 +303,7 @@ type DeletePodQuery struct {
func (m *DeletePodQuery) Reset() { *m = DeletePodQuery{} }
func (m *DeletePodQuery) String() string { return proto.CompactTextString(m) }
func (*DeletePodQuery) ProtoMessage() {}
func (*DeletePodQuery) Descriptor() ([]byte, []int) { return fileDescriptorApplication, []int{7} }
func (*DeletePodQuery) Descriptor() ([]byte, []int) { return fileDescriptorApplication, []int{8} }
func (m *DeletePodQuery) GetApplicationName() string {
if m != nil {
@@ -304,7 +332,7 @@ type PodLogsQuery struct {
func (m *PodLogsQuery) Reset() { *m = PodLogsQuery{} }
func (m *PodLogsQuery) String() string { return proto.CompactTextString(m) }
func (*PodLogsQuery) ProtoMessage() {}
func (*PodLogsQuery) Descriptor() ([]byte, []int) { return fileDescriptorApplication, []int{8} }
func (*PodLogsQuery) Descriptor() ([]byte, []int) { return fileDescriptorApplication, []int{9} }
func (m *PodLogsQuery) GetApplicationName() string {
if m != nil {
@@ -363,7 +391,7 @@ type LogEntry struct {
func (m *LogEntry) Reset() { *m = LogEntry{} }
func (m *LogEntry) String() string { return proto.CompactTextString(m) }
func (*LogEntry) ProtoMessage() {}
func (*LogEntry) Descriptor() ([]byte, []int) { return fileDescriptorApplication, []int{9} }
func (*LogEntry) Descriptor() ([]byte, []int) { return fileDescriptorApplication, []int{10} }
func (m *LogEntry) GetContent() string {
if m != nil {
@@ -384,6 +412,7 @@ func init() {
proto.RegisterType((*ApplicationResponse)(nil), "application.ApplicationResponse")
proto.RegisterType((*DeleteApplicationRequest)(nil), "application.DeleteApplicationRequest")
proto.RegisterType((*ApplicationSyncRequest)(nil), "application.ApplicationSyncRequest")
proto.RegisterType((*ApplicationSpecRequest)(nil), "application.ApplicationSpecRequest")
proto.RegisterType((*ApplicationSyncResult)(nil), "application.ApplicationSyncResult")
proto.RegisterType((*ApplicationRollbackRequest)(nil), "application.ApplicationRollbackRequest")
proto.RegisterType((*ResourceDetails)(nil), "application.ResourceDetails")
@@ -413,6 +442,8 @@ type ApplicationServiceClient interface {
Get(ctx context.Context, in *ApplicationQuery, opts ...grpc.CallOption) (*github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.Application, error)
// Update updates an application
Update(ctx context.Context, in *github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.Application, opts ...grpc.CallOption) (*github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.Application, error)
// Update updates an application spec
UpdateSpec(ctx context.Context, in *ApplicationSpecRequest, opts ...grpc.CallOption) (*github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.ApplicationSpec, error)
// Delete deletes an application
Delete(ctx context.Context, in *DeleteApplicationRequest, opts ...grpc.CallOption) (*ApplicationResponse, error)
// Sync syncs an application to its target state
@@ -501,6 +532,15 @@ func (c *applicationServiceClient) Update(ctx context.Context, in *github_com_ar
return out, nil
}
func (c *applicationServiceClient) UpdateSpec(ctx context.Context, in *ApplicationSpecRequest, opts ...grpc.CallOption) (*github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.ApplicationSpec, error) {
out := new(github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.ApplicationSpec)
err := grpc.Invoke(ctx, "/application.ApplicationService/UpdateSpec", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *applicationServiceClient) Delete(ctx context.Context, in *DeleteApplicationRequest, opts ...grpc.CallOption) (*ApplicationResponse, error) {
out := new(ApplicationResponse)
err := grpc.Invoke(ctx, "/application.ApplicationService/Delete", in, out, c.cc, opts...)
@@ -582,6 +622,8 @@ type ApplicationServiceServer interface {
Get(context.Context, *ApplicationQuery) (*github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.Application, error)
// Update updates an application
Update(context.Context, *github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.Application) (*github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.Application, error)
// Update updates an application spec
UpdateSpec(context.Context, *ApplicationSpecRequest) (*github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.ApplicationSpec, error)
// Delete deletes an application
Delete(context.Context, *DeleteApplicationRequest) (*ApplicationResponse, error)
// Sync syncs an application to its target state
@@ -691,6 +733,24 @@ func _ApplicationService_Update_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler)
}
func _ApplicationService_UpdateSpec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ApplicationSpecRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApplicationServiceServer).UpdateSpec(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/application.ApplicationService/UpdateSpec",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApplicationServiceServer).UpdateSpec(ctx, req.(*ApplicationSpecRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ApplicationService_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteApplicationRequest)
if err := dec(in); err != nil {
@@ -804,6 +864,10 @@ var _ApplicationService_serviceDesc = grpc.ServiceDesc{
MethodName: "Update",
Handler: _ApplicationService_Update_Handler,
},
{
MethodName: "UpdateSpec",
Handler: _ApplicationService_UpdateSpec_Handler,
},
{
MethodName: "Delete",
Handler: _ApplicationService_Delete_Handler,
@@ -974,6 +1038,40 @@ func (m *ApplicationSyncRequest) MarshalTo(dAtA []byte) (int, error) {
return i, nil
}
func (m *ApplicationSpecRequest) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *ApplicationSpecRequest) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.AppName) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintApplication(dAtA, i, uint64(len(m.AppName)))
i += copy(dAtA[i:], m.AppName)
}
if m.Spec != nil {
dAtA[i] = 0x12
i++
i = encodeVarintApplication(dAtA, i, uint64(m.Spec.Size()))
n1, err := m.Spec.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n1
}
return i, nil
}
func (m *ApplicationSyncResult) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
@@ -1173,11 +1271,11 @@ func (m *PodLogsQuery) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x2a
i++
i = encodeVarintApplication(dAtA, i, uint64(m.SinceTime.Size()))
n1, err := m.SinceTime.MarshalTo(dAtA[i:])
n2, err := m.SinceTime.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n1
i += n2
}
if m.TailLines != 0 {
dAtA[i] = 0x30
@@ -1222,11 +1320,11 @@ func (m *LogEntry) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12
i++
i = encodeVarintApplication(dAtA, i, uint64(m.TimeStamp.Size()))
n2, err := m.TimeStamp.MarshalTo(dAtA[i:])
n3, err := m.TimeStamp.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n2
i += n3
}
return i, nil
}
@@ -1297,6 +1395,20 @@ func (m *ApplicationSyncRequest) Size() (n int) {
return n
}
func (m *ApplicationSpecRequest) Size() (n int) {
var l int
_ = l
l = len(m.AppName)
if l > 0 {
n += 1 + l + sovApplication(uint64(l))
}
if m.Spec != nil {
l = m.Spec.Size()
n += 1 + l + sovApplication(uint64(l))
}
return n
}
func (m *ApplicationSyncResult) Size() (n int) {
var l int
_ = l
@@ -1860,6 +1972,118 @@ func (m *ApplicationSyncRequest) Unmarshal(dAtA []byte) error {
}
return nil
}
func (m *ApplicationSpecRequest) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowApplication
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: ApplicationSpecRequest: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: ApplicationSpecRequest: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field AppName", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowApplication
}
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 ErrInvalidLengthApplication
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.AppName = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Spec", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowApplication
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthApplication
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Spec == nil {
m.Spec = &github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1.ApplicationSpec{}
}
if err := m.Spec.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipApplication(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthApplication
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *ApplicationSyncResult) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
@@ -2830,67 +3054,71 @@ var (
func init() { proto.RegisterFile("server/application/application.proto", fileDescriptorApplication) }
var fileDescriptorApplication = []byte{
// 990 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x56, 0x41, 0x6f, 0xdc, 0x44,
0x14, 0x96, 0x77, 0x37, 0x9b, 0xdd, 0x49, 0x45, 0xd1, 0x90, 0x44, 0xae, 0x9b, 0xa6, 0xab, 0x69,
0x0a, 0x4b, 0x10, 0x76, 0x13, 0x40, 0xa0, 0xa8, 0x1c, 0x08, 0x29, 0x50, 0x14, 0xa1, 0xe0, 0x14,
0x21, 0x71, 0x41, 0x13, 0x7b, 0xea, 0x98, 0xb5, 0x67, 0xdc, 0x99, 0x59, 0xa3, 0xa5, 0xe4, 0x00,
0x27, 0x38, 0x21, 0x04, 0x77, 0x7e, 0x07, 0xff, 0x00, 0x6e, 0x48, 0xdc, 0x11, 0x8a, 0xf8, 0x0b,
0xdc, 0xd1, 0x8c, 0xed, 0xb5, 0xbd, 0xd9, 0xdd, 0x36, 0x68, 0x0f, 0x3d, 0x79, 0xde, 0x9b, 0x37,
0xef, 0xfb, 0xde, 0x7b, 0x33, 0xef, 0x19, 0x6c, 0x09, 0xc2, 0x53, 0xc2, 0x1d, 0x9c, 0x24, 0x51,
0xe8, 0x61, 0x19, 0x32, 0x5a, 0x5d, 0xdb, 0x09, 0x67, 0x92, 0xc1, 0x95, 0x8a, 0xca, 0x5a, 0x0d,
0x58, 0xc0, 0xb4, 0xde, 0x51, 0xab, 0xcc, 0xc4, 0xda, 0x08, 0x18, 0x0b, 0x22, 0xe2, 0xe0, 0x24,
0x74, 0x30, 0xa5, 0x4c, 0x6a, 0x63, 0x91, 0xef, 0xa2, 0xc1, 0x5b, 0xc2, 0x0e, 0x99, 0xde, 0xf5,
0x18, 0x27, 0x4e, 0xba, 0xe3, 0x04, 0x84, 0x12, 0x8e, 0x25, 0xf1, 0x73, 0x9b, 0xd7, 0x4b, 0x9b,
0x18, 0x7b, 0xa7, 0x21, 0x25, 0x7c, 0xe4, 0x24, 0x83, 0x40, 0x29, 0x84, 0x13, 0x13, 0x89, 0xa7,
0x9d, 0xba, 0x1f, 0x84, 0xf2, 0x74, 0x78, 0x62, 0x7b, 0x2c, 0x76, 0x30, 0xd7, 0xc4, 0xbe, 0xd0,
0x8b, 0x57, 0x3d, 0xbf, 0x3c, 0x5d, 0x0d, 0x2f, 0xdd, 0xc1, 0x51, 0x72, 0x8a, 0x2f, 0xb8, 0x42,
0x2f, 0x82, 0xe7, 0xdf, 0x29, 0xed, 0x3e, 0x1e, 0x12, 0x3e, 0x82, 0x10, 0xb4, 0x28, 0x8e, 0x89,
0x69, 0xf4, 0x8c, 0x7e, 0xd7, 0xd5, 0x6b, 0xb4, 0x06, 0x5e, 0xa8, 0xd8, 0xb9, 0x44, 0x24, 0x8c,
0x0a, 0x82, 0xbe, 0x02, 0xe6, 0x01, 0x89, 0x88, 0x24, 0xb5, 0xcd, 0x47, 0x43, 0x22, 0xe4, 0x34,
0x37, 0x70, 0x03, 0x74, 0xd5, 0x57, 0x24, 0xd8, 0x23, 0x66, 0x43, 0x6f, 0x94, 0x0a, 0xb8, 0x0e,
0xda, 0x59, 0x69, 0xcc, 0xa6, 0xde, 0xca, 0x25, 0xb8, 0x0a, 0x96, 0x1e, 0x32, 0xee, 0x11, 0xb3,
0xd5, 0x33, 0xfa, 0x1d, 0x37, 0x13, 0x50, 0x0a, 0xd6, 0x2b, 0xa8, 0xc7, 0x23, 0xea, 0xcd, 0x43,
0xb6, 0x40, 0x87, 0x93, 0x34, 0x14, 0x21, 0xa3, 0x39, 0xf0, 0x58, 0x56, 0xb8, 0x3e, 0x1f, 0xb9,
0x43, 0xaa, 0x71, 0x3b, 0x6e, 0x2e, 0x29, 0xdc, 0x84, 0x0f, 0xe9, 0x18, 0x57, 0x0b, 0x28, 0x06,
0x6b, 0x17, 0x70, 0xc5, 0x30, 0x92, 0xd0, 0x04, 0xcb, 0x31, 0x11, 0x02, 0x07, 0x05, 0x72, 0x21,
0xc2, 0x3d, 0xd0, 0xe5, 0x44, 0xb0, 0x21, 0xf7, 0x88, 0x30, 0x1b, 0xbd, 0x66, 0x7f, 0x65, 0x77,
0xc3, 0xae, 0x5e, 0x39, 0x37, 0xdf, 0x3d, 0x20, 0x12, 0x87, 0x91, 0x70, 0x4b, 0x73, 0x94, 0x02,
0xab, 0x9a, 0x5c, 0x16, 0x45, 0x27, 0xd8, 0x1b, 0xcc, 0x0b, 0x75, 0x1d, 0x34, 0x42, 0x5f, 0x07,
0xd9, 0xdc, 0x6f, 0x9f, 0xff, 0x75, 0xb3, 0x71, 0xff, 0xc0, 0x6d, 0x84, 0xfe, 0x25, 0xc3, 0x7c,
0x04, 0xae, 0x4e, 0xb0, 0x9a, 0x0a, 0x06, 0x41, 0x6b, 0x10, 0x52, 0x3f, 0xcf, 0xa9, 0x5e, 0xd7,
0xab, 0xdc, 0x9c, 0xac, 0x72, 0x25, 0x4d, 0xad, 0x5a, 0x9a, 0xd0, 0x03, 0xf0, 0x5c, 0x76, 0x9b,
0x8e, 0x98, 0x9f, 0x5d, 0xc5, 0x3e, 0xb8, 0x5a, 0x49, 0xd3, 0x47, 0x25, 0xf8, 0xa4, 0x5a, 0x79,
0x4d, 0x98, 0xaf, 0x2d, 0x32, 0x2a, 0x85, 0x88, 0x7e, 0x6e, 0x80, 0x2b, 0x47, 0xcc, 0x3f, 0x64,
0x81, 0x58, 0x98, 0x53, 0x15, 0xa2, 0xc7, 0xa8, 0xc4, 0xea, 0xc5, 0x16, 0x21, 0x8e, 0x15, 0x10,
0x81, 0x2b, 0x22, 0xa4, 0x1e, 0x39, 0x26, 0x1e, 0xa3, 0xbe, 0xd0, 0x71, 0x36, 0xdd, 0x9a, 0x0e,
0x7e, 0x00, 0xba, 0x5a, 0x7e, 0x10, 0xc6, 0xc4, 0x5c, 0xea, 0x19, 0xfd, 0x95, 0xdd, 0x6d, 0x3b,
0x6b, 0x07, 0x76, 0xb5, 0x1d, 0xd8, 0xc9, 0x20, 0x50, 0x0a, 0x61, 0xab, 0x76, 0x60, 0xa7, 0x3b,
0xb6, 0x3a, 0xe1, 0x96, 0x87, 0x15, 0x17, 0x55, 0x9f, 0xc3, 0x90, 0x12, 0x61, 0xb6, 0x35, 0x54,
0xa9, 0x50, 0x55, 0x7f, 0xc8, 0xa2, 0x88, 0x7d, 0x69, 0x2e, 0x67, 0x55, 0xcf, 0x24, 0x44, 0x41,
0xe7, 0x90, 0x05, 0xf7, 0xa8, 0xe4, 0x23, 0x15, 0xa7, 0x22, 0x4f, 0xa8, 0x2c, 0x6e, 0x6e, 0x2e,
0x2a, 0x96, 0x32, 0x8c, 0xc9, 0xb1, 0xc4, 0x71, 0xa2, 0x73, 0x70, 0x49, 0x96, 0xe3, 0xc3, 0xbb,
0xff, 0xae, 0x00, 0x58, 0x7d, 0x37, 0x84, 0xa7, 0xa1, 0x47, 0xe0, 0x0f, 0x06, 0x68, 0x1d, 0x86,
0x42, 0xc2, 0x1b, 0xb5, 0x07, 0x31, 0xd9, 0x94, 0xac, 0x0f, 0xed, 0xb2, 0xe9, 0xd9, 0x45, 0xd3,
0xd3, 0x8b, 0xcf, 0x3d, 0xbf, 0x44, 0xaf, 0xfa, 0x28, 0x9a, 0x5e, 0xd5, 0x99, 0x82, 0x42, 0x1b,
0xdf, 0xfe, 0xf9, 0xcf, 0x4f, 0x8d, 0x75, 0xb8, 0xaa, 0x7b, 0x73, 0xba, 0x53, 0x6d, 0x95, 0x02,
0xfe, 0x62, 0x80, 0xa5, 0x4f, 0xb1, 0xf4, 0x4e, 0x9f, 0x44, 0xe9, 0x68, 0x31, 0x94, 0x34, 0xd6,
0xbd, 0x94, 0x50, 0x89, 0x6e, 0x69, 0x62, 0x37, 0xe0, 0xf5, 0x82, 0x98, 0x90, 0x9c, 0xe0, 0xb8,
0xc6, 0xef, 0x8e, 0x01, 0x7f, 0x35, 0x40, 0xfb, 0x5d, 0x4e, 0xb0, 0x24, 0xf0, 0xbd, 0xc5, 0x70,
0xb0, 0x16, 0xe4, 0x07, 0xdd, 0xd4, 0x11, 0x5c, 0x43, 0x53, 0x53, 0xbb, 0x67, 0x6c, 0xc3, 0x1f,
0x0d, 0xd0, 0x7c, 0x9f, 0x3c, 0xb1, 0xdc, 0x8b, 0xe2, 0x73, 0x21, 0xa3, 0x55, 0x3e, 0xce, 0x63,
0xd5, 0x95, 0xce, 0xe0, 0xef, 0x06, 0x68, 0x7f, 0x92, 0xf8, 0xcf, 0x62, 0x3e, 0x1d, 0xcd, 0xff,
0x65, 0x6b, 0x6b, 0x3a, 0x7f, 0xf5, 0xd8, 0x7c, 0x2c, 0xb1, 0xad, 0x03, 0x51, 0xf9, 0x4d, 0x41,
0x3b, 0xeb, 0xa1, 0xf0, 0x76, 0xcd, 0xfb, 0xac, 0x31, 0x6d, 0xf5, 0x66, 0x15, 0x62, 0x3c, 0xe4,
0xf3, 0x1c, 0x6e, 0xcf, 0xcd, 0xe1, 0xd7, 0xa0, 0xa5, 0x46, 0x21, 0xbc, 0x35, 0xcb, 0x5d, 0x65,
0x40, 0x5b, 0x68, 0xbe, 0x91, 0x9a, 0xa6, 0xe8, 0x15, 0x8d, 0x7a, 0x1b, 0xf5, 0xe6, 0xa0, 0x3a,
0x62, 0x44, 0x3d, 0x15, 0xf5, 0x77, 0x06, 0xe8, 0x14, 0xa3, 0x11, 0xbe, 0x34, 0x33, 0xa2, 0xfa,
0xf0, 0x7c, 0x2a, 0x1a, 0x79, 0x01, 0xd0, 0xd6, 0x3c, 0x1a, 0x3c, 0x77, 0xac, 0xa8, 0x7c, 0x6f,
0x80, 0xee, 0x78, 0x8a, 0xc1, 0xeb, 0x53, 0x8a, 0x50, 0x4c, 0xb7, 0xa7, 0x48, 0xfd, 0xdb, 0x1a,
0xfd, 0xcd, 0xed, 0x37, 0xa6, 0xa3, 0x4f, 0xcc, 0xab, 0x33, 0x27, 0x61, 0xbe, 0x70, 0x1e, 0xe7,
0x43, 0xea, 0x0c, 0x7e, 0x63, 0x80, 0xe5, 0x7c, 0xf4, 0xc1, 0x6b, 0x35, 0xb0, 0xea, 0x40, 0xb4,
0xd6, 0x6a, 0x5b, 0xc5, 0x54, 0x40, 0xfb, 0x1a, 0xfc, 0x2e, 0xdc, 0xfb, 0x5f, 0xe0, 0x4e, 0xc4,
0x02, 0x71, 0xc7, 0xd8, 0xbf, 0xfb, 0xdb, 0xf9, 0xa6, 0xf1, 0xc7, 0xf9, 0xa6, 0xf1, 0xf7, 0xf9,
0xa6, 0xf1, 0x99, 0x3d, 0xef, 0xd7, 0xf5, 0xe2, 0x7f, 0xf9, 0x49, 0x5b, 0xff, 0xa6, 0xbe, 0xf6,
0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x69, 0xad, 0xd8, 0xee, 0xb4, 0x0b, 0x00, 0x00,
// 1054 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x57, 0x4f, 0x6f, 0xdc, 0x44,
0x14, 0x97, 0x77, 0x37, 0x9b, 0x64, 0x52, 0x28, 0x1a, 0x92, 0xc8, 0x75, 0xd3, 0x34, 0x9a, 0xa4,
0x10, 0x82, 0xb0, 0x9b, 0x00, 0x02, 0x45, 0xe5, 0x40, 0x48, 0x81, 0xa2, 0x08, 0x05, 0xa7, 0x08,
0x89, 0x03, 0x68, 0x62, 0xbf, 0x3a, 0x26, 0xf6, 0x8c, 0xeb, 0x99, 0x35, 0x5a, 0x4a, 0x0e, 0x70,
0x82, 0x13, 0xe2, 0xcf, 0x9d, 0x13, 0x47, 0x3e, 0x00, 0xdf, 0x00, 0x6e, 0x48, 0xdc, 0x11, 0x8a,
0xf8, 0x20, 0x68, 0xc6, 0xf6, 0xda, 0xde, 0xec, 0x6e, 0x5b, 0xb4, 0x87, 0x9e, 0x76, 0xde, 0xcc,
0xf3, 0xfc, 0x7e, 0xef, 0xbd, 0x99, 0xf7, 0x9b, 0x45, 0x1b, 0x02, 0xd2, 0x0c, 0x52, 0x87, 0x26,
0x49, 0x14, 0x7a, 0x54, 0x86, 0x9c, 0xd5, 0xc7, 0x76, 0x92, 0x72, 0xc9, 0xf1, 0x42, 0x6d, 0xca,
0x5a, 0x0c, 0x78, 0xc0, 0xf5, 0xbc, 0xa3, 0x46, 0xb9, 0x8b, 0xb5, 0x12, 0x70, 0x1e, 0x44, 0xe0,
0xd0, 0x24, 0x74, 0x28, 0x63, 0x5c, 0x6a, 0x67, 0x51, 0xac, 0x92, 0xd3, 0xd7, 0x85, 0x1d, 0x72,
0xbd, 0xea, 0xf1, 0x14, 0x9c, 0x6c, 0xdb, 0x09, 0x80, 0x41, 0x4a, 0x25, 0xf8, 0x85, 0xcf, 0x2b,
0x95, 0x4f, 0x4c, 0xbd, 0x93, 0x90, 0x41, 0xda, 0x77, 0x92, 0xd3, 0x40, 0x4d, 0x08, 0x27, 0x06,
0x49, 0x47, 0x7d, 0x75, 0x27, 0x08, 0xe5, 0x49, 0xef, 0xd8, 0xf6, 0x78, 0xec, 0xd0, 0x54, 0x13,
0xfb, 0x4c, 0x0f, 0x5e, 0xf2, 0xfc, 0xea, 0xeb, 0x7a, 0x78, 0xd9, 0x36, 0x8d, 0x92, 0x13, 0x7a,
0x61, 0x2b, 0xf2, 0x1c, 0x7a, 0xe6, 0xcd, 0xca, 0xef, 0x83, 0x1e, 0xa4, 0x7d, 0x8c, 0x51, 0x87,
0xd1, 0x18, 0x4c, 0x63, 0xcd, 0xd8, 0x9c, 0x77, 0xf5, 0x98, 0x2c, 0xa1, 0x67, 0x6b, 0x7e, 0x2e,
0x88, 0x84, 0x33, 0x01, 0xe4, 0x0b, 0x64, 0xee, 0x43, 0x04, 0x12, 0x1a, 0x8b, 0xf7, 0x7b, 0x20,
0xe4, 0xa8, 0x6d, 0xf0, 0x0a, 0x9a, 0x57, 0xbf, 0x22, 0xa1, 0x1e, 0x98, 0x2d, 0xbd, 0x50, 0x4d,
0xe0, 0x65, 0xd4, 0xcd, 0x4b, 0x63, 0xb6, 0xf5, 0x52, 0x61, 0xe1, 0x45, 0x34, 0x73, 0x8f, 0xa7,
0x1e, 0x98, 0x9d, 0x35, 0x63, 0x73, 0xce, 0xcd, 0x0d, 0x92, 0xa1, 0xe5, 0x1a, 0xea, 0x51, 0x9f,
0x79, 0x93, 0x90, 0x2d, 0x34, 0x97, 0x42, 0x16, 0x8a, 0x90, 0xb3, 0x02, 0x78, 0x60, 0x2b, 0x5c,
0x3f, 0xed, 0xbb, 0x3d, 0xa6, 0x71, 0xe7, 0xdc, 0xc2, 0x52, 0xb8, 0x49, 0xda, 0x63, 0x03, 0x5c,
0x6d, 0x90, 0x1f, 0x8c, 0x26, 0x70, 0x02, 0x03, 0x60, 0x13, 0xcd, 0xd2, 0x24, 0x79, 0xbf, 0xc2,
0x2e, 0x4d, 0xfc, 0x09, 0xea, 0x88, 0x04, 0x3c, 0x0d, 0xbd, 0xb0, 0xf3, 0x9e, 0x5d, 0x55, 0xd0,
0x2e, 0x2b, 0xa8, 0x07, 0x9f, 0x7a, 0xbe, 0x9d, 0x9c, 0x06, 0xb6, 0xaa, 0xa0, 0x5d, 0x3f, 0x94,
0x65, 0x05, 0xed, 0x61, 0x68, 0xbd, 0x2f, 0x89, 0xd1, 0xd2, 0x85, 0x64, 0x88, 0x5e, 0xa4, 0x29,
0xc5, 0x20, 0x04, 0x0d, 0x06, 0x94, 0x0a, 0x13, 0xef, 0xa2, 0xf9, 0x14, 0x04, 0xef, 0xa5, 0x1e,
0x08, 0xb3, 0xb5, 0xd6, 0xde, 0x5c, 0xd8, 0x59, 0x69, 0x40, 0xba, 0xc5, 0xea, 0x3e, 0x48, 0x1a,
0x46, 0xc2, 0xad, 0xdc, 0x49, 0x86, 0xac, 0x7a, 0xc5, 0x79, 0x14, 0x1d, 0x53, 0xef, 0x74, 0x52,
0xfe, 0x97, 0x51, 0x2b, 0xf4, 0x75, 0xf8, 0xed, 0xbd, 0xee, 0xf9, 0xdf, 0xd7, 0x5b, 0x77, 0xf6,
0xdd, 0x56, 0xe8, 0x3f, 0x66, 0xee, 0xef, 0xa3, 0xcb, 0x43, 0xac, 0x46, 0x82, 0x61, 0xd4, 0x39,
0x0d, 0x99, 0x5f, 0x14, 0x5a, 0x8f, 0x9b, 0x47, 0xaf, 0x3d, 0x7c, 0xf4, 0x6a, 0x69, 0xea, 0x34,
0xd2, 0x44, 0xee, 0xa2, 0xa7, 0xf3, 0x23, 0x7e, 0xc8, 0xfd, 0xfc, 0x7e, 0x6c, 0xa2, 0xcb, 0xb5,
0x34, 0xd5, 0xaa, 0x3d, 0x3c, 0xad, 0x76, 0x4d, 0xb8, 0xaf, 0x3d, 0x72, 0x2a, 0xa5, 0x49, 0x7e,
0x6a, 0xa1, 0x4b, 0x87, 0xdc, 0x3f, 0xe0, 0x81, 0x98, 0xda, 0xa6, 0x2a, 0x44, 0x8f, 0x33, 0x49,
0x55, 0x1b, 0x29, 0x43, 0x1c, 0x4c, 0x60, 0x82, 0x2e, 0x89, 0x90, 0x79, 0x70, 0x04, 0x1e, 0x67,
0xbe, 0xd0, 0x71, 0xb6, 0xdd, 0xc6, 0x1c, 0x7e, 0x17, 0xcd, 0x6b, 0xfb, 0x6e, 0x18, 0x83, 0x39,
0xa3, 0xcf, 0xea, 0x96, 0x9d, 0xf7, 0x28, 0xbb, 0xde, 0xa3, 0xaa, 0x33, 0xaa, 0x7a, 0x94, 0x9d,
0x6d, 0xdb, 0xea, 0x0b, 0xb7, 0xfa, 0x58, 0x71, 0x51, 0xf5, 0x39, 0x08, 0x19, 0x08, 0xb3, 0xab,
0xa1, 0xaa, 0x09, 0x55, 0xf5, 0x7b, 0x3c, 0x8a, 0xf8, 0xe7, 0xe6, 0x6c, 0x5e, 0xf5, 0xdc, 0x22,
0x0c, 0xcd, 0x1d, 0xf0, 0xe0, 0x36, 0x93, 0x69, 0x5f, 0xc5, 0xa9, 0xc8, 0x03, 0x93, 0xe5, 0xc9,
0x2d, 0x4c, 0xc5, 0x52, 0x86, 0x31, 0x1c, 0x49, 0x1a, 0x27, 0xc5, 0x8d, 0x7a, 0x2c, 0x96, 0x83,
0x8f, 0x77, 0x7e, 0x79, 0x0a, 0xe1, 0xfa, 0xbd, 0x81, 0x34, 0x0b, 0x3d, 0xc0, 0xdf, 0x19, 0xa8,
0x73, 0x10, 0x0a, 0x89, 0xaf, 0x35, 0x2e, 0xc4, 0x70, 0xa7, 0xb4, 0xa6, 0x74, 0x8f, 0x15, 0x14,
0x59, 0xf9, 0xfa, 0xaf, 0x7f, 0x7f, 0x6c, 0x2d, 0xe3, 0x45, 0x2d, 0x18, 0xd9, 0x76, 0xbd, 0x7f,
0x0b, 0xfc, 0xb3, 0x81, 0x66, 0x3e, 0xa2, 0xd2, 0x3b, 0x79, 0x18, 0xa5, 0xc3, 0xe9, 0x50, 0xd2,
0x58, 0xb7, 0x33, 0x60, 0x92, 0xac, 0x6b, 0x62, 0xd7, 0xf0, 0xd5, 0x92, 0x98, 0x90, 0x29, 0xd0,
0xb8, 0xc1, 0xef, 0xa6, 0x81, 0x7f, 0x33, 0x50, 0xf7, 0xad, 0x14, 0xa8, 0x04, 0xfc, 0xf6, 0x74,
0x38, 0x58, 0x53, 0xda, 0x87, 0x5c, 0xd7, 0x11, 0x5c, 0x21, 0x23, 0x53, 0xbb, 0x6b, 0x6c, 0xe1,
0xef, 0x0d, 0xd4, 0x7e, 0x07, 0x1e, 0x5a, 0xee, 0x69, 0xf1, 0xb9, 0x90, 0xd1, 0x3a, 0x1f, 0xe7,
0x81, 0xea, 0x4a, 0x67, 0xf8, 0x0f, 0x03, 0x75, 0x3f, 0x4c, 0xfc, 0x27, 0x31, 0x9f, 0x8e, 0xe6,
0xff, 0x82, 0xb5, 0x31, 0x9a, 0xbf, 0xba, 0x6c, 0x3e, 0x95, 0xd4, 0xd6, 0x81, 0xa8, 0xfc, 0xfe,
0x6a, 0x20, 0x94, 0xc7, 0xa2, 0x24, 0x0b, 0xaf, 0x8f, 0x4b, 0x73, 0x4d, 0x4b, 0xad, 0x29, 0x6a,
0x24, 0xb1, 0x35, 0xe1, 0x4d, 0x6b, 0x7d, 0x34, 0xe1, 0x42, 0xa4, 0xcf, 0x1c, 0x25, 0xa5, 0x8a,
0x6f, 0x86, 0xba, 0x79, 0xcf, 0xc7, 0x37, 0x1a, 0x00, 0xe3, 0xde, 0x3a, 0xd6, 0xda, 0xb8, 0x88,
0x06, 0x2f, 0xa5, 0xa2, 0xe6, 0x5b, 0x13, 0x6b, 0xfe, 0x25, 0xea, 0x28, 0xe9, 0x9e, 0x90, 0xa0,
0xea, 0x95, 0x63, 0x91, 0xc9, 0x4e, 0x4a, 0xfd, 0xc9, 0x8b, 0x1a, 0xf5, 0x06, 0x59, 0x9b, 0x80,
0xea, 0x88, 0x3e, 0xd3, 0x51, 0x7f, 0x63, 0xa0, 0xb9, 0x52, 0xca, 0xf1, 0xf3, 0x63, 0x23, 0x6a,
0x8a, 0xfd, 0x23, 0xd1, 0x28, 0x0e, 0x0c, 0xd9, 0x98, 0x44, 0x23, 0x2d, 0x36, 0x56, 0x54, 0xbe,
0x35, 0xd0, 0xfc, 0x40, 0x75, 0xf1, 0xd5, 0x11, 0x45, 0x28, 0xd5, 0xf8, 0x11, 0x52, 0xff, 0x86,
0x46, 0x7f, 0x6d, 0xeb, 0xd5, 0xb1, 0xd5, 0xaf, 0xeb, 0xeb, 0x99, 0x93, 0x70, 0x5f, 0x38, 0x0f,
0x0a, 0x51, 0x3d, 0xc3, 0x5f, 0x19, 0x68, 0xb6, 0x90, 0x6a, 0x7c, 0xa5, 0x01, 0x56, 0x17, 0x70,
0x6b, 0xa9, 0xb1, 0x54, 0xaa, 0x18, 0xd9, 0xd3, 0xe0, 0xb7, 0xf0, 0xee, 0xff, 0x02, 0x77, 0x22,
0x1e, 0x88, 0x9b, 0xc6, 0xde, 0xad, 0xdf, 0xcf, 0x57, 0x8d, 0x3f, 0xcf, 0x57, 0x8d, 0x7f, 0xce,
0x57, 0x8d, 0x8f, 0xed, 0x49, 0xef, 0xff, 0x8b, 0x7f, 0x6e, 0x8e, 0xbb, 0xfa, 0xad, 0xff, 0xf2,
0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x61, 0x8e, 0xe6, 0xda, 0xf9, 0x0c, 0x00, 0x00,
}

View File

@@ -142,6 +142,37 @@ func request_ApplicationService_Update_0(ctx context.Context, marshaler runtime.
}
func request_ApplicationService_UpdateSpec_0(ctx context.Context, marshaler runtime.Marshaler, client ApplicationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ApplicationSpecRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["appName"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "appName")
}
protoReq.AppName, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "appName", err)
}
msg, err := client.UpdateSpec(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
var (
filter_ApplicationService_Delete_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
)
@@ -514,6 +545,35 @@ func RegisterApplicationServiceHandlerClient(ctx context.Context, mux *runtime.S
})
mux.Handle("PUT", pattern_ApplicationService_UpdateSpec_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_ApplicationService_UpdateSpec_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_ApplicationService_UpdateSpec_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("DELETE", pattern_ApplicationService_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -673,6 +733,8 @@ var (
pattern_ApplicationService_Update_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "applications", "metadata.name"}, ""))
pattern_ApplicationService_UpdateSpec_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "applications", "appName", "spec"}, ""))
pattern_ApplicationService_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "applications", "name"}, ""))
pattern_ApplicationService_Sync_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "applications", "name", "sync"}, ""))
@@ -695,6 +757,8 @@ var (
forward_ApplicationService_Update_0 = runtime.ForwardResponseMessage
forward_ApplicationService_UpdateSpec_0 = runtime.ForwardResponseMessage
forward_ApplicationService_Delete_0 = runtime.ForwardResponseMessage
forward_ApplicationService_Sync_0 = runtime.ForwardResponseMessage

View File

@@ -35,6 +35,12 @@ message ApplicationSyncRequest {
bool prune = 4;
}
// ApplicationSpecRequest is a request to update application spec
message ApplicationSpecRequest {
string appName = 1;
github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.ApplicationSpec spec = 2;
}
// ApplicationSyncResult is a result of a sync requeswt
message ApplicationSyncResult {
string message = 1;
@@ -109,6 +115,14 @@ service ApplicationService {
};
}
// Update updates an application spec
rpc UpdateSpec(ApplicationSpecRequest) returns (github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.ApplicationSpec) {
option (google.api.http) = {
put: "/api/v1/applications/{appName}/spec"
body: "*"
};
}
// Delete deletes an application
rpc Delete(DeleteApplicationRequest) returns (ApplicationResponse) {
option (google.api.http).delete = "/api/v1/applications/{name}";

View File

@@ -12,8 +12,8 @@ import (
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
"github.com/argoproj/argo-cd/util/kube"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
apiv1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -63,7 +63,7 @@ func (s *Server) List(ctx context.Context, q *ClusterQuery) (*appv1.ClusterList,
Items: make([]appv1.Cluster, len(clusterSecrets.Items)),
}
for i, clusterSecret := range clusterSecrets.Items {
clusterList.Items[i] = *secretToCluster(&clusterSecret)
clusterList.Items[i] = *secretToCluster(&clusterSecret, false)
}
return &clusterList, nil
}
@@ -87,11 +87,11 @@ func (s *Server) Create(ctx context.Context, c *appv1.Cluster) (*appv1.Cluster,
clusterSecret, err = s.kubeclientset.CoreV1().Secrets(s.ns).Create(clusterSecret)
if err != nil {
if apierr.IsAlreadyExists(err) {
return nil, grpc.Errorf(codes.AlreadyExists, "cluster '%s' already exists", c.Server)
return nil, status.Errorf(codes.AlreadyExists, "cluster '%s' already exists", c.Server)
}
return nil, err
}
return secretToCluster(clusterSecret), nil
return secretToCluster(clusterSecret, false), nil
}
// ClusterEvent contains information about cluster event
@@ -120,7 +120,7 @@ func (s *Server) WatchClusters(ctx context.Context, callback func(*ClusterEvent)
}()
for next := range w.ResultChan() {
secret := next.Object.(*apiv1.Secret)
cluster := secretToCluster(secret)
cluster := secretToCluster(secret, false)
callback(&ClusterEvent{
Type: next.Type,
Cluster: cluster,
@@ -134,7 +134,7 @@ func (s *Server) getClusterSecret(server string) (*apiv1.Secret, error) {
clusterSecret, err := s.kubeclientset.CoreV1().Secrets(s.ns).Get(secName, metav1.GetOptions{})
if err != nil {
if apierr.IsNotFound(err) {
return nil, grpc.Errorf(codes.NotFound, "cluster '%s' not found", server)
return nil, status.Errorf(codes.NotFound, "cluster '%s' not found", server)
}
return nil, err
}
@@ -147,7 +147,7 @@ func (s *Server) Get(ctx context.Context, q *ClusterQuery) (*appv1.Cluster, erro
if err != nil {
return nil, err
}
return secretToCluster(clusterSecret), nil
return secretToCluster(clusterSecret, false), nil
}
// Update updates a cluster
@@ -165,7 +165,7 @@ func (s *Server) Update(ctx context.Context, c *appv1.Cluster) (*appv1.Cluster,
if err != nil {
return nil, err
}
return secretToCluster(clusterSecret), nil
return secretToCluster(clusterSecret, false), nil
}
// UpdateREST updates a cluster (special handler intended to be used only by the gRPC gateway)
@@ -210,8 +210,8 @@ func clusterToStringData(c *appv1.Cluster) map[string]string {
return stringData
}
// secretToRepo converts a secret into a repository object
func secretToCluster(s *apiv1.Secret) *appv1.Cluster {
// secretToCluster converts a secret into a repository object, optionally redacting sensitive information
func secretToCluster(s *apiv1.Secret, redact bool) *appv1.Cluster {
var config appv1.ClusterConfig
err := json.Unmarshal(s.Data["config"], &config)
if err != nil {
@@ -220,7 +220,9 @@ func secretToCluster(s *apiv1.Secret) *appv1.Cluster {
cluster := appv1.Cluster{
Server: string(s.Data["server"]),
Name: string(s.Data["name"]),
Config: config,
}
if !redact {
cluster.Config = config
}
return &cluster
}

View File

@@ -10,8 +10,8 @@ import (
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
"github.com/argoproj/argo-cd/util/git"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
apiv1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -54,7 +54,7 @@ func (s *Server) List(ctx context.Context, q *RepoQuery) (*appsv1.RepositoryList
Items: make([]appsv1.Repository, len(repoSecrets.Items)),
}
for i, repoSec := range repoSecrets.Items {
repoList.Items[i] = *secretToRepo(&repoSec)
repoList.Items[i] = *secretToRepo(&repoSec, true)
}
return &repoList, nil
}
@@ -81,11 +81,11 @@ func (s *Server) Create(ctx context.Context, r *appsv1.Repository) (*appsv1.Repo
repoSecret, err = s.kubeclientset.CoreV1().Secrets(s.ns).Create(repoSecret)
if err != nil {
if apierr.IsAlreadyExists(err) {
return nil, grpc.Errorf(codes.AlreadyExists, "repository '%s' already exists", r.Repo)
return nil, status.Errorf(codes.AlreadyExists, "repository '%s' already exists", r.Repo)
}
return nil, err
}
return secretToRepo(repoSecret), nil
return secretToRepo(repoSecret, false), nil
}
func (s *Server) getRepoSecret(repo string) (*apiv1.Secret, error) {
@@ -93,7 +93,7 @@ func (s *Server) getRepoSecret(repo string) (*apiv1.Secret, error) {
repoSecret, err := s.kubeclientset.CoreV1().Secrets(s.ns).Get(secName, metav1.GetOptions{})
if err != nil {
if apierr.IsNotFound(err) {
return nil, grpc.Errorf(codes.NotFound, "repo '%s' not found", repo)
return nil, status.Errorf(codes.NotFound, "repo '%s' not found", repo)
}
return nil, err
}
@@ -106,7 +106,7 @@ func (s *Server) Get(ctx context.Context, q *RepoQuery) (*appsv1.Repository, err
if err != nil {
return nil, err
}
return secretToRepo(repoSecret), nil
return secretToRepo(repoSecret, false), nil
}
// Update updates a repository
@@ -124,7 +124,7 @@ func (s *Server) Update(ctx context.Context, r *appsv1.Repository) (*appsv1.Repo
if err != nil {
return nil, err
}
return secretToRepo(repoSecret), nil
return secretToRepo(repoSecret, false), nil
}
// UpdateREST updates a repository (from a REST request)
@@ -159,12 +159,15 @@ func repoToStringData(r *appsv1.Repository) map[string]string {
}
}
// secretToRepo converts a secret into a repository object
func secretToRepo(s *apiv1.Secret) *appsv1.Repository {
return &appsv1.Repository{
Repo: string(s.Data["repository"]),
Username: string(s.Data["username"]),
Password: string(s.Data["password"]),
SSHPrivateKey: string(s.Data["sshPrivateKey"]),
// secretToRepo converts a secret into a repository object, optionally redacting sensitive information
func secretToRepo(s *apiv1.Secret, redact bool) *appsv1.Repository {
repo := appsv1.Repository{
Repo: string(s.Data["repository"]),
Username: string(s.Data["username"]),
}
if !redact {
repo.Password = string(s.Data["password"])
repo.SSHPrivateKey = string(s.Data["sshPrivateKey"])
}
return &repo
}

View File

@@ -8,20 +8,25 @@ import (
"net"
"net/http"
"strings"
"time"
argocd "github.com/argoproj/argo-cd"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/pkg/apiclient"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
"github.com/argoproj/argo-cd/reposerver"
"github.com/argoproj/argo-cd/server/application"
"github.com/argoproj/argo-cd/server/cluster"
"github.com/argoproj/argo-cd/server/repository"
"github.com/argoproj/argo-cd/server/session"
"github.com/argoproj/argo-cd/server/settings"
"github.com/argoproj/argo-cd/server/version"
"github.com/argoproj/argo-cd/util/config"
dexutil "github.com/argoproj/argo-cd/util/dex"
grpc_util "github.com/argoproj/argo-cd/util/grpc"
jsonutil "github.com/argoproj/argo-cd/util/json"
util_session "github.com/argoproj/argo-cd/util/session"
settings_util "github.com/argoproj/argo-cd/util/settings"
tlsutil "github.com/argoproj/argo-cd/util/tls"
golang_proto "github.com/golang/protobuf/proto"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
@@ -36,24 +41,29 @@ import (
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
)
const (
port = 8080
authCookieName = "argocd.argoproj.io/auth-token"
port = 8080
)
var (
endpoint = fmt.Sprintf("localhost:%d", port)
// ErrNoSession indicates no auth token was supplied as part of a request
ErrNoSession = status.Errorf(codes.Unauthenticated, "no session information")
)
// ArgoCDServer is the API server for ArgoCD
type ArgoCDServer struct {
ArgoCDServerOpts
settings config.ArgoCDSettings
log *log.Entry
ssoClientApp *dexutil.ClientApp
settings settings_util.ArgoCDSettings
log *log.Entry
sessionMgr *util_session.SessionManager
settingsMgr *settings_util.SettingsManager
}
type ArgoCDServerOpts struct {
@@ -68,15 +78,18 @@ type ArgoCDServerOpts struct {
// NewServer returns a new instance of the ArgoCD API server
func NewServer(opts ArgoCDServerOpts) *ArgoCDServer {
configManager := config.NewConfigManager(opts.KubeClientset, opts.Namespace)
settings, err := configManager.GetSettings()
settingsMgr := settings_util.NewSettingsManager(opts.KubeClientset, opts.Namespace)
settings, err := settingsMgr.GetSettings()
if err != nil {
log.Fatal(err)
}
sessionMgr := util_session.MakeSessionManager(settings.ServerSignature)
return &ArgoCDServer{
ArgoCDServerOpts: opts,
log: log.NewEntry(log.New()),
settings: *settings,
sessionMgr: &sessionMgr,
settingsMgr: settingsMgr,
}
}
@@ -122,24 +135,57 @@ func (a *ArgoCDServer) Run() {
}
tlsl = tls.NewListener(tlsl, &tlsConfig)
// Now, we build another mux recursively to match HTTPS and GoRPC.
// Now, we build another mux recursively to match HTTPS and gRPC.
tlsm = cmux.New(tlsl)
httpsL = tlsm.Match(cmux.HTTP1Fast())
grpcL = tlsm.Match(cmux.Any())
}
// Start the muxed listeners for our servers
log.Infof("argocd %s serving on port %d (tls: %v, namespace: %s)", argocd.GetVersion(), port, a.useTLS(), a.Namespace)
log.Infof("argocd %s serving on port %d (url: %s, tls: %v, namespace: %s, sso: %v)",
argocd.GetVersion(), port, a.settings.URL, a.useTLS(), a.Namespace, a.settings.IsSSOConfigured())
go func() { errors.CheckError(grpcS.Serve(grpcL)) }()
go func() { errors.CheckError(httpS.Serve(httpL)) }()
if a.useTLS() {
go func() { errors.CheckError(httpsS.Serve(httpsL)) }()
go func() { errors.CheckError(tlsm.Serve()) }()
}
go a.initializeOIDCClientApp()
err = tcpm.Serve()
errors.CheckError(err)
}
// initializeOIDCClientApp initializes the OIDC Client application, querying the well known oidc
// configuration path. Because ArgoCD is a OIDC client to itself, we have a chicken-and-egg problem
// of (1) serving dex over HTTP, and (2) querying the OIDC provider (ourselves) to initialize the
// app (HTTP GET http://example-argocd.com/api/dex/.well-known/openid-configuration)
// This method is expected to be invoked right after we start listening over HTTP
func (a *ArgoCDServer) initializeOIDCClientApp() {
if !a.settings.IsSSOConfigured() {
return
}
// wait for dex to become ready
dexClient, err := dexutil.NewDexClient()
errors.CheckError(err)
dexClient.WaitUntilReady()
var backoff = wait.Backoff{
Steps: 5,
Duration: 1 * time.Second,
Factor: 1.0,
Jitter: 0.1,
}
var realErr error
_ = wait.ExponentialBackoff(backoff, func() (bool, error) {
realErr = a.ssoClientApp.Initialize()
if realErr != nil {
a.log.Warnf("failed to initialize client app: %v", realErr)
return false, nil
}
return true, nil
})
errors.CheckError(realErr)
}
func (a *ArgoCDServer) useTLS() bool {
if a.Insecure || a.settings.Certificate == nil {
return false
@@ -167,35 +213,29 @@ func (a *ArgoCDServer) newGRPCServer() *grpc.Server {
grpcS := grpc.NewServer(sOpts...)
clusterService := cluster.NewServer(a.Namespace, a.KubeClientset, a.AppClientset)
repoService := repository.NewServer(a.Namespace, a.KubeClientset, a.AppClientset)
sessionService := session.NewServer(a.Namespace, a.KubeClientset, a.AppClientset, a.settings)
sessionService := session.NewServer(a.settings)
applicationService := application.NewServer(a.Namespace, a.KubeClientset, a.AppClientset, a.RepoClientset, repoService, clusterService)
settingsService := settings.NewServer(a.settingsMgr)
version.RegisterVersionServiceServer(grpcS, &version.Server{})
cluster.RegisterClusterServiceServer(grpcS, clusterService)
application.RegisterApplicationServiceServer(grpcS, applicationService)
repository.RegisterRepositoryServiceServer(grpcS, repoService)
session.RegisterSessionServiceServer(grpcS, sessionService)
settings.RegisterSettingsServiceServer(grpcS, settingsService)
// Register reflection service on gRPC server.
reflection.Register(grpcS)
return grpcS
}
// MakeCookieMetadata generates a string representing a Web cookie. Yum!
func (a *ArgoCDServer) makeCookieMetadata(key, value string, flags ...string) string {
components := []string{
fmt.Sprintf("%s=%s", key, value),
}
if a.ArgoCDServerOpts.Insecure == false {
components = append(components, "Secure")
}
components = append(components, flags...)
return strings.Join(components, "; ")
}
// TranslateGrpcCookieHeader conditionally sets a cookie on the response.
func (a *ArgoCDServer) translateGrpcCookieHeader(ctx context.Context, w http.ResponseWriter, resp golang_proto.Message) error {
if sessionResp, ok := resp.(*session.SessionResponse); ok {
cookie := a.makeCookieMetadata(authCookieName, sessionResp.Token, "path=/")
flags := []string{"path=/"}
if !a.Insecure {
flags = append(flags, "Secure")
}
cookie := util_session.MakeCookieMetadata(common.AuthCookieName, sessionResp.Token, flags...)
w.Header().Set("Set-Cookie", cookie)
}
return nil
@@ -215,16 +255,9 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context) *http.Server {
// grpc-gateway is just translating HTTP/HTTPS requests as gRPC requests over localhost,
// so we need to supply the same certificates to establish the connections that a normal,
// external gRPC client would need.
certPool := x509.NewCertPool()
pemCertBytes, _ := tlsutil.EncodeX509KeyPair(*a.settings.Certificate)
ok := certPool.AppendCertsFromPEM(pemCertBytes)
if !ok {
panic("bad certs")
}
dCreds := credentials.NewTLS(&tls.Config{
RootCAs: certPool,
InsecureSkipVerify: true,
})
tlsConfig := a.selfTLSConfig()
tlsConfig.InsecureSkipVerify = true
dCreds := credentials.NewTLS(tlsConfig)
dOpts = append(dOpts, grpc.WithTransportCredentials(dCreds))
} else {
dOpts = append(dOpts, grpc.WithInsecure())
@@ -245,6 +278,9 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context) *http.Server {
mustRegisterGWHandler(application.RegisterApplicationServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
mustRegisterGWHandler(repository.RegisterRepositoryServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
mustRegisterGWHandler(session.RegisterSessionServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
mustRegisterGWHandler(settings.RegisterSettingsServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
a.registerDexHandlers(mux)
if a.StaticAssetsDir != "" {
mux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
@@ -268,6 +304,35 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context) *http.Server {
return &httpS
}
// selfTLSConfig returns a tls.Config with the configured certificates
func (a *ArgoCDServer) selfTLSConfig() *tls.Config {
certPool := x509.NewCertPool()
pemCertBytes, _ := tlsutil.EncodeX509KeyPair(*a.settings.Certificate)
ok := certPool.AppendCertsFromPEM(pemCertBytes)
if !ok {
panic("bad certs")
}
return &tls.Config{
RootCAs: certPool,
}
}
// registerDexHandlers will register dex HTTP handlers, creating the the OAuth client app
func (a *ArgoCDServer) registerDexHandlers(mux *http.ServeMux) {
if !a.settings.IsSSOConfigured() {
return
}
// Run dex OpenID Connect Identity Provider behind a reverse proxy (served at /api/dex)
var err error
mux.HandleFunc(dexutil.DexAPIEndpoint+"/", dexutil.NewDexHTTPReverseProxy())
tlsConfig := a.selfTLSConfig()
tlsConfig.InsecureSkipVerify = true
a.ssoClientApp, err = dexutil.NewClientApp(a.settings.URL, a.settings.ServerSignature, tlsConfig)
errors.CheckError(err)
mux.HandleFunc(dexutil.LoginEndpoint, a.ssoClientApp.HandleLogin)
mux.HandleFunc(dexutil.CallbackEndpoint, a.ssoClientApp.HandleCallback)
}
// newRedirectServer returns an HTTP server which does a 307 redirect to the HTTPS server
func newRedirectServer() *http.Server {
return &http.Server{
@@ -292,41 +357,45 @@ func mustRegisterGWHandler(register registerFunc, ctx context.Context, mux *runt
}
}
// parseTokens tests a slice of strings and returns `true` only if any of them are valid.
func (a *ArgoCDServer) parseTokens(tokens []string) bool {
mgr := util_session.MakeSessionManager(a.settings.ServerSignature)
for _, token := range tokens {
_, err := mgr.Parse(token)
if err == nil {
return true
}
}
return false
}
// Authenticate checks for the presence of a token when accessing server-side resources.
// Authenticate checks for the presence of a valid token when accessing server-side resources.
func (a *ArgoCDServer) authenticate(ctx context.Context) (context.Context, error) {
if a.DisableAuth {
return ctx, nil
}
if md, ok := metadata.FromIncomingContext(ctx); ok {
tokens := md["tokens"]
// Extract only the value portion of cookie-stored tokens
for _, cookieToken := range md["grpcgateway-cookie"] {
tokenPair := strings.SplitN(cookieToken, "=", 2)
if len(tokenPair) == 2 {
tokens = append(tokens, tokenPair[1])
}
}
// Check both gRPC-provided tokens and Web-provided (cookie-based) ones
if a.parseTokens(tokens) {
return ctx, nil
}
return ctx, status.Errorf(codes.Unauthenticated, "user is not allowed access")
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ctx, ErrNoSession
}
return ctx, status.Errorf(codes.Unauthenticated, "empty metadata")
token := getToken(md)
if token == "" {
return ctx, ErrNoSession
}
_, err := a.sessionMgr.Parse(token)
if err != nil {
return ctx, fmt.Errorf("failed to validate auth token: %v", err)
}
// TODO: when we care about user groups, we will want to put the claims into the context
return ctx, nil
}
// getToken extracts the token from gRPC metadata or cookie headers
func getToken(md metadata.MD) string {
// check the "token" metadata
tokens, ok := md[apiclient.MetaDataTokenKey]
if ok && len(tokens) > 0 {
return tokens[0]
}
// check the legacy key (v0.3.2 and below). 'tokens' was renamed to 'token'
tokens, ok = md["tokens"]
if ok && len(tokens) > 0 {
return tokens[0]
}
// check the HTTP cookie
for _, cookieToken := range md["grpcgateway-cookie"] {
tokenPair := strings.SplitN(cookieToken, "=", 2)
if len(tokenPair) == 2 && tokenPair[0] == common.AuthCookieName {
return tokenPair[1]
}
}
return ""
}

View File

@@ -3,29 +3,21 @@ package session
import (
"context"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
"github.com/argoproj/argo-cd/util/config"
"github.com/argoproj/argo-cd/util/password"
"github.com/argoproj/argo-cd/util/session"
"github.com/argoproj/argo-cd/util/settings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"k8s.io/client-go/kubernetes"
)
// Server provides a Session service
type Server struct {
ns string
kubeclientset kubernetes.Interface
appclientset appclientset.Interface
serversettings config.ArgoCDSettings
serversettings settings.ArgoCDSettings
}
// NewServer returns a new instance of the Session service
func NewServer(namespace string, kubeclientset kubernetes.Interface, appclientset appclientset.Interface, serversettings config.ArgoCDSettings) *Server {
func NewServer(serversettings settings.ArgoCDSettings) *Server {
return &Server{
ns: namespace,
appclientset: appclientset,
kubeclientset: kubeclientset,
serversettings: serversettings,
}
}

View File

@@ -0,0 +1,36 @@
package settings
import (
"github.com/argoproj/argo-cd/util/settings"
"github.com/ghodss/yaml"
"golang.org/x/net/context"
)
// Server provides a Settings service
type Server struct {
mgr *settings.SettingsManager
}
// NewServer returns a new instance of the Repository service
func NewServer(mgr *settings.SettingsManager) *Server {
return &Server{
mgr: mgr,
}
}
// Get returns ArgoCD settings
func (s *Server) Get(ctx context.Context, q *SettingsQuery) (*Settings, error) {
argoCDSettings, err := s.mgr.GetSettings()
if err != nil {
return nil, err
}
set := Settings{
URL: argoCDSettings.URL,
}
var cfg DexConfig
err = yaml.Unmarshal([]byte(argoCDSettings.DexConfig), &cfg)
if err == nil {
set.DexConfig = &cfg
}
return &set, nil
}

View File

@@ -0,0 +1,859 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: server/settings/settings.proto
/*
Package settings is a generated protocol buffer package.
Settings Service
Settings Service API retrives ArgoCD settings
It is generated from these files:
server/settings/settings.proto
It has these top-level messages:
SettingsQuery
Settings
DexConfig
Connector
*/
package settings
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import _ "github.com/gogo/protobuf/gogoproto"
import _ "google.golang.org/genproto/googleapis/api/annotations"
import context "golang.org/x/net/context"
import grpc "google.golang.org/grpc"
import io "io"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
// SettingsQuery is a query for ArgoCD settings
type SettingsQuery struct {
}
func (m *SettingsQuery) Reset() { *m = SettingsQuery{} }
func (m *SettingsQuery) String() string { return proto.CompactTextString(m) }
func (*SettingsQuery) ProtoMessage() {}
func (*SettingsQuery) Descriptor() ([]byte, []int) { return fileDescriptorSettings, []int{0} }
type Settings struct {
URL string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
DexConfig *DexConfig `protobuf:"bytes,2,opt,name=dexConfig" json:"dexConfig,omitempty"`
}
func (m *Settings) Reset() { *m = Settings{} }
func (m *Settings) String() string { return proto.CompactTextString(m) }
func (*Settings) ProtoMessage() {}
func (*Settings) Descriptor() ([]byte, []int) { return fileDescriptorSettings, []int{1} }
func (m *Settings) GetURL() string {
if m != nil {
return m.URL
}
return ""
}
func (m *Settings) GetDexConfig() *DexConfig {
if m != nil {
return m.DexConfig
}
return nil
}
type DexConfig struct {
Connectors []*Connector `protobuf:"bytes,1,rep,name=connectors" json:"connectors,omitempty"`
}
func (m *DexConfig) Reset() { *m = DexConfig{} }
func (m *DexConfig) String() string { return proto.CompactTextString(m) }
func (*DexConfig) ProtoMessage() {}
func (*DexConfig) Descriptor() ([]byte, []int) { return fileDescriptorSettings, []int{2} }
func (m *DexConfig) GetConnectors() []*Connector {
if m != nil {
return m.Connectors
}
return nil
}
type Connector struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
}
func (m *Connector) Reset() { *m = Connector{} }
func (m *Connector) String() string { return proto.CompactTextString(m) }
func (*Connector) ProtoMessage() {}
func (*Connector) Descriptor() ([]byte, []int) { return fileDescriptorSettings, []int{3} }
func (m *Connector) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Connector) GetType() string {
if m != nil {
return m.Type
}
return ""
}
func init() {
proto.RegisterType((*SettingsQuery)(nil), "cluster.SettingsQuery")
proto.RegisterType((*Settings)(nil), "cluster.Settings")
proto.RegisterType((*DexConfig)(nil), "cluster.DexConfig")
proto.RegisterType((*Connector)(nil), "cluster.Connector")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for SettingsService service
type SettingsServiceClient interface {
// Get returns ArgoCD settings
Get(ctx context.Context, in *SettingsQuery, opts ...grpc.CallOption) (*Settings, error)
}
type settingsServiceClient struct {
cc *grpc.ClientConn
}
func NewSettingsServiceClient(cc *grpc.ClientConn) SettingsServiceClient {
return &settingsServiceClient{cc}
}
func (c *settingsServiceClient) Get(ctx context.Context, in *SettingsQuery, opts ...grpc.CallOption) (*Settings, error) {
out := new(Settings)
err := grpc.Invoke(ctx, "/cluster.SettingsService/Get", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for SettingsService service
type SettingsServiceServer interface {
// Get returns ArgoCD settings
Get(context.Context, *SettingsQuery) (*Settings, error)
}
func RegisterSettingsServiceServer(s *grpc.Server, srv SettingsServiceServer) {
s.RegisterService(&_SettingsService_serviceDesc, srv)
}
func _SettingsService_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SettingsQuery)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SettingsServiceServer).Get(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/cluster.SettingsService/Get",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SettingsServiceServer).Get(ctx, req.(*SettingsQuery))
}
return interceptor(ctx, in, info, handler)
}
var _SettingsService_serviceDesc = grpc.ServiceDesc{
ServiceName: "cluster.SettingsService",
HandlerType: (*SettingsServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Get",
Handler: _SettingsService_Get_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "server/settings/settings.proto",
}
func (m *SettingsQuery) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *SettingsQuery) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
return i, nil
}
func (m *Settings) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *Settings) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.URL) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintSettings(dAtA, i, uint64(len(m.URL)))
i += copy(dAtA[i:], m.URL)
}
if m.DexConfig != nil {
dAtA[i] = 0x12
i++
i = encodeVarintSettings(dAtA, i, uint64(m.DexConfig.Size()))
n1, err := m.DexConfig.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n1
}
return i, nil
}
func (m *DexConfig) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *DexConfig) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.Connectors) > 0 {
for _, msg := range m.Connectors {
dAtA[i] = 0xa
i++
i = encodeVarintSettings(dAtA, i, uint64(msg.Size()))
n, err := msg.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n
}
}
return i, nil
}
func (m *Connector) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *Connector) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.Name) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintSettings(dAtA, i, uint64(len(m.Name)))
i += copy(dAtA[i:], m.Name)
}
if len(m.Type) > 0 {
dAtA[i] = 0x12
i++
i = encodeVarintSettings(dAtA, i, uint64(len(m.Type)))
i += copy(dAtA[i:], m.Type)
}
return i, nil
}
func encodeVarintSettings(dAtA []byte, offset int, v uint64) int {
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
}
func (m *SettingsQuery) Size() (n int) {
var l int
_ = l
return n
}
func (m *Settings) Size() (n int) {
var l int
_ = l
l = len(m.URL)
if l > 0 {
n += 1 + l + sovSettings(uint64(l))
}
if m.DexConfig != nil {
l = m.DexConfig.Size()
n += 1 + l + sovSettings(uint64(l))
}
return n
}
func (m *DexConfig) Size() (n int) {
var l int
_ = l
if len(m.Connectors) > 0 {
for _, e := range m.Connectors {
l = e.Size()
n += 1 + l + sovSettings(uint64(l))
}
}
return n
}
func (m *Connector) Size() (n int) {
var l int
_ = l
l = len(m.Name)
if l > 0 {
n += 1 + l + sovSettings(uint64(l))
}
l = len(m.Type)
if l > 0 {
n += 1 + l + sovSettings(uint64(l))
}
return n
}
func sovSettings(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
}
func sozSettings(x uint64) (n int) {
return sovSettings(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *SettingsQuery) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: SettingsQuery: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: SettingsQuery: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
default:
iNdEx = preIndex
skippy, err := skipSettings(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthSettings
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *Settings) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: Settings: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: Settings: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field URL", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
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 ErrInvalidLengthSettings
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.URL = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field DexConfig", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthSettings
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.DexConfig == nil {
m.DexConfig = &DexConfig{}
}
if err := m.DexConfig.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipSettings(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthSettings
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *DexConfig) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: DexConfig: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: DexConfig: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Connectors", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthSettings
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Connectors = append(m.Connectors, &Connector{})
if err := m.Connectors[len(m.Connectors)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipSettings(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthSettings
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *Connector) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: Connector: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: Connector: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
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 ErrInvalidLengthSettings
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Name = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Type", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
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 ErrInvalidLengthSettings
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Type = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipSettings(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthSettings
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipSettings(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowSettings
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowSettings
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowSettings
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthSettings
}
return iNdEx, nil
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowSettings
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipSettings(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
case 4:
return iNdEx, nil
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
}
panic("unreachable")
}
var (
ErrInvalidLengthSettings = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowSettings = fmt.Errorf("proto: integer overflow")
)
func init() { proto.RegisterFile("server/settings/settings.proto", fileDescriptorSettings) }
var fileDescriptorSettings = []byte{
// 322 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0x41, 0x4b, 0xc3, 0x40,
0x10, 0x85, 0xd9, 0x46, 0xac, 0x19, 0x91, 0xea, 0x22, 0x12, 0x8b, 0xc4, 0x92, 0x53, 0x41, 0x4c,
0xb4, 0x3d, 0x79, 0x12, 0x5a, 0x41, 0x10, 0x2f, 0xa6, 0x88, 0x20, 0x78, 0x48, 0xd3, 0x71, 0x8d,
0xb4, 0x3b, 0x65, 0xb3, 0x29, 0xf6, 0xea, 0x5f, 0xf0, 0x4f, 0x79, 0x14, 0xbc, 0x8b, 0x04, 0x7f,
0x88, 0x74, 0xdb, 0x44, 0xab, 0xb7, 0xc7, 0xf7, 0x66, 0x92, 0xb7, 0xf3, 0xc0, 0x4d, 0x51, 0x4d,
0x50, 0x05, 0x29, 0x6a, 0x9d, 0x48, 0x91, 0x96, 0xc2, 0x1f, 0x2b, 0xd2, 0xc4, 0xab, 0xf1, 0x30,
0x4b, 0x35, 0xaa, 0xfa, 0xb6, 0x20, 0x41, 0x86, 0x05, 0x33, 0x35, 0xb7, 0xeb, 0x7b, 0x82, 0x48,
0x0c, 0x31, 0x88, 0xc6, 0x49, 0x10, 0x49, 0x49, 0x3a, 0xd2, 0x09, 0xc9, 0xc5, 0xb2, 0x57, 0x83,
0x8d, 0xde, 0xe2, 0x73, 0x57, 0x19, 0xaa, 0xa9, 0x77, 0x03, 0x6b, 0x05, 0xe0, 0xbb, 0x60, 0x65,
0x6a, 0xe8, 0xb0, 0x06, 0x6b, 0xda, 0x9d, 0x6a, 0xfe, 0xb1, 0x6f, 0x5d, 0x87, 0x97, 0xe1, 0x8c,
0xf1, 0x23, 0xb0, 0x07, 0xf8, 0xd4, 0x25, 0x79, 0x9f, 0x08, 0xa7, 0xd2, 0x60, 0xcd, 0xf5, 0x16,
0xf7, 0x17, 0x41, 0xfc, 0xb3, 0xc2, 0x09, 0x7f, 0x86, 0xbc, 0x53, 0xb0, 0x4b, 0xce, 0x5b, 0x00,
0x31, 0x49, 0x89, 0xb1, 0x26, 0x95, 0x3a, 0xac, 0x61, 0x2d, 0xed, 0x77, 0x0b, 0x2b, 0xfc, 0x35,
0xe5, 0xb5, 0xc1, 0x2e, 0x0d, 0xce, 0x61, 0x45, 0x46, 0x23, 0x9c, 0x67, 0x0b, 0x8d, 0x9e, 0x31,
0x3d, 0x1d, 0xa3, 0x89, 0x63, 0x87, 0x46, 0xb7, 0xee, 0xa0, 0x56, 0x3c, 0xa7, 0x87, 0x6a, 0x92,
0xc4, 0xc8, 0x2f, 0xc0, 0x3a, 0x47, 0xcd, 0x77, 0xca, 0xdf, 0x2d, 0x1d, 0xa0, 0xbe, 0xf5, 0x8f,
0x7b, 0xce, 0xf3, 0xfb, 0xd7, 0x4b, 0x85, 0xf3, 0x4d, 0x73, 0xc4, 0xc9, 0x71, 0xd9, 0x40, 0xe7,
0xe4, 0x35, 0x77, 0xd9, 0x5b, 0xee, 0xb2, 0xcf, 0xdc, 0x65, 0xb7, 0x07, 0x22, 0xd1, 0x0f, 0x59,
0xdf, 0x8f, 0x69, 0x14, 0x44, 0xca, 0x74, 0xf1, 0x68, 0xc4, 0x61, 0x3c, 0x08, 0xfe, 0xb4, 0xd8,
0x5f, 0x35, 0x05, 0xb4, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0xef, 0x0e, 0xd5, 0xb9, 0xdf, 0x01,
0x00, 0x00,
}

View File

@@ -0,0 +1,116 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: server/settings/settings.proto
/*
Package settings is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package settings
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
func request_SettingsService_Get_0(ctx context.Context, marshaler runtime.Marshaler, client SettingsServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq SettingsQuery
var metadata runtime.ServerMetadata
msg, err := client.Get(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
// RegisterSettingsServiceHandlerFromEndpoint is same as RegisterSettingsServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterSettingsServiceHandlerFromEndpoint(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 RegisterSettingsServiceHandler(ctx, mux, conn)
}
// RegisterSettingsServiceHandler registers the http handlers for service SettingsService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterSettingsServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterSettingsServiceHandlerClient(ctx, mux, NewSettingsServiceClient(conn))
}
// RegisterSettingsServiceHandler registers the http handlers for service SettingsService to "mux".
// The handlers forward requests to the grpc endpoint over the given implementation of "SettingsServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "SettingsServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "SettingsServiceClient" to call the correct interceptors.
func RegisterSettingsServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client SettingsServiceClient) error {
mux.Handle("GET", pattern_SettingsService_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_SettingsService_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_SettingsService_Get_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_SettingsService_Get_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "settings"}, ""))
)
var (
forward_SettingsService_Get_0 = runtime.ForwardResponseMessage
)

View File

@@ -0,0 +1,38 @@
syntax = "proto3";
option go_package = "github.com/argoproj/argo-cd/server/settings";
// Settings Service
//
// Settings Service API retrives ArgoCD settings
package cluster;
import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
// SettingsQuery is a query for ArgoCD settings
message SettingsQuery {
}
message Settings {
string url = 1 [(gogoproto.customname) = "URL"];;
DexConfig dexConfig = 2;
}
message DexConfig {
repeated Connector connectors = 1;
}
message Connector {
string name = 1;
string type = 2;
}
// SettingsService
service SettingsService {
// Get returns ArgoCD settings
rpc Get(SettingsQuery) returns (Settings) {
option (google.api.http).get = "/api/v1/settings";
}
}

View File

@@ -1,150 +0,0 @@
package config
import (
"crypto/tls"
"fmt"
"github.com/argoproj/argo-cd/common"
tlsutil "github.com/argoproj/argo-cd/util/tls"
apiv1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// ArgoCDSettings holds in-memory runtime configuration options.
type ArgoCDSettings struct {
// LocalUsers holds users local to (stored on) the server. This is to be distinguished from any potential alternative future login providers (LDAP, SAML, etc.) that might ever be added.
LocalUsers map[string]string
// ServerSignature holds the key used to generate JWT tokens.
ServerSignature []byte
// Certificate holds the certificate/private key for the ArgoCD API server.
// If nil, will run insecure without TLS.
Certificate *tls.Certificate
}
const (
// configManagerAdminPasswordKey designates the key for a root password inside a Kubernetes secret.
configManagerAdminPasswordKey = "admin.password"
// configManagerServerSignatureKey designates the key for a server secret key inside a Kubernetes secret.
configManagerServerSignatureKey = "server.secretkey"
// configManagerServerCertificate designates the key for the public cert used in TLS
configManagerServerCertificate = "server.crt"
// configManagerServerPrivateKey designates the key for the private key used in TLS
configManagerServerPrivateKey = "server.key"
)
// ConfigManager holds config info for a new manager with which to access Kubernetes ConfigMaps.
type ConfigManager struct {
clientset kubernetes.Interface
namespace string
}
// GetSettings retrieves settings from the ConfigManager.
func (mgr *ConfigManager) GetSettings() (*ArgoCDSettings, error) {
// TODO: we currently do not store anything in configmaps, yet. We eventually will (e.g.
// tuning parameters). Future settings/tunables should be stored here
_, err := mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Get(common.ArgoCDConfigMapName, metav1.GetOptions{})
if err != nil {
return nil, err
}
argoCDSecret, err := mgr.clientset.CoreV1().Secrets(mgr.namespace).Get(common.ArgoCDSecretName, metav1.GetOptions{})
if err != nil {
return nil, err
}
var settings ArgoCDSettings
adminPasswordHash, ok := argoCDSecret.Data[configManagerAdminPasswordKey]
if !ok {
return nil, fmt.Errorf("admin user not found")
}
settings.LocalUsers = map[string]string{
common.ArgoCDAdminUsername: string(adminPasswordHash),
}
secretKey, ok := argoCDSecret.Data[configManagerServerSignatureKey]
if !ok {
return nil, fmt.Errorf("server secret key not found")
}
settings.ServerSignature = secretKey
serverCert, certOk := argoCDSecret.Data[configManagerServerCertificate]
serverKey, keyOk := argoCDSecret.Data[configManagerServerPrivateKey]
if certOk && keyOk {
cert, err := tls.X509KeyPair(serverCert, serverKey)
if err != nil {
return nil, fmt.Errorf("invalid x509 key pair %s/%s in secret: %s", configManagerServerCertificate, configManagerServerPrivateKey, err)
}
settings.Certificate = &cert
}
return &settings, nil
}
// SaveSettings serializes ArgoCD settings and upserts it into K8s secret/configmap
func (mgr *ConfigManager) SaveSettings(settings *ArgoCDSettings) error {
configMapData := make(map[string]string)
_, err := mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Get(common.ArgoCDConfigMapName, metav1.GetOptions{})
if err != nil {
if !apierr.IsNotFound(err) {
return err
}
newConfigMap := &apiv1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: common.ArgoCDConfigMapName,
},
Data: configMapData,
}
_, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Create(newConfigMap)
if err != nil {
return err
}
} else {
// mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Update()
}
secretStringData := map[string]string{
configManagerServerSignatureKey: string(settings.ServerSignature),
configManagerAdminPasswordKey: settings.LocalUsers[common.ArgoCDAdminUsername],
}
if settings.Certificate != nil {
certBytes, keyBytes := tlsutil.EncodeX509KeyPair(*settings.Certificate)
secretStringData[configManagerServerCertificate] = string(certBytes)
secretStringData[configManagerServerPrivateKey] = string(keyBytes)
}
argoCDSecret, err := mgr.clientset.CoreV1().Secrets(mgr.namespace).Get(common.ArgoCDSecretName, metav1.GetOptions{})
if err != nil {
if !apierr.IsNotFound(err) {
return err
}
newSecret := &apiv1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: common.ArgoCDSecretName,
},
StringData: secretStringData,
}
_, err = mgr.clientset.CoreV1().Secrets(mgr.namespace).Create(newSecret)
if err != nil {
return err
}
} else {
argoCDSecret.Data = nil
argoCDSecret.StringData = secretStringData
_, err = mgr.clientset.CoreV1().Secrets(mgr.namespace).Update(argoCDSecret)
if err != nil {
return err
}
}
return nil
}
// NewConfigManager generates a new ConfigManager pointer and returns it
func NewConfigManager(clientset kubernetes.Interface, namespace string) *ConfigManager {
return &ConfigManager{
clientset: clientset,
namespace: namespace,
}
}

177
util/dex/config.go Normal file
View File

@@ -0,0 +1,177 @@
package dex
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/util/settings"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
const (
// DexClientAppName is name of the Oauth client app used when registering our app to dex
DexClientAppName = "ArgoCD"
// DexClientAppID is the Oauth client ID we will use when registering our app to dex
DexClientAppID = "argo-cd"
)
func GenerateDexConfigYAML(kubeClientset kubernetes.Interface, namespace string) ([]byte, error) {
settingsMgr := settings.NewSettingsManager(kubeClientset, namespace)
settings, err := settingsMgr.GetSettings()
if err != nil {
return nil, err
}
if !settings.IsSSOConfigured() {
return nil, nil
}
var dexCfg map[string]interface{}
err = yaml.Unmarshal([]byte(settings.DexConfig), &dexCfg)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal dex.config from configmap: %v", err)
}
dexCfg["issuer"] = settings.URL + DexAPIEndpoint
dexCfg["storage"] = map[string]interface{}{
"type": "memory",
}
dexCfg["web"] = map[string]interface{}{
"http": "0.0.0.0:5556",
}
dexCfg["grpc"] = map[string]interface{}{
"addr": "0.0.0.0:5557",
}
dexCfg["oauth2"] = map[string]interface{}{
"skipApprovalScreen": true,
}
dexCfg["staticClients"] = []map[string]interface{}{
{
"id": DexClientAppID,
"name": DexClientAppName,
"secret": formulateOAuthClientSecret(settings.ServerSignature),
"redirectURIs": []string{
settings.URL + CallbackEndpoint,
},
},
}
// dexCfg["enablePasswordDB"] = true
// dexCfg["staticPasswords"] = []map[string]interface{}{
// {
// "userID": "00000000-0000-0000-0000-000000000001",
// "username": "admin",
// "email": "admin@internal",
// "hash": settings.LocalUsers["admin"],
// },
// }
connectors := dexCfg["connectors"].([]interface{})
for i, connectorIf := range connectors {
connector := connectorIf.(map[string]interface{})
connectorType := connector["type"].(string)
if !needsRedirectURI(connectorType) {
continue
}
connectorCfg := connector["config"].(map[string]interface{})
connectorCfg["redirectURI"] = settings.URL + "/api/dex/callback"
connector["config"] = connectorCfg
connectors[i] = connector
}
dexCfg["connectors"] = connectors
secretValues, err := getSecretValues(kubeClientset, namespace)
if err != nil {
return nil, err
}
dexCfg = replaceMapSecrets(dexCfg, secretValues)
return yaml.Marshal(dexCfg)
}
// formulateOAuthClientSecret calculates an arbitrary, but predictable OAuth2 client secret string
// derived some seed input (typically the server secret). This is called by the dex startup wrapper
// (argocd-util rundex), as well as the API server, such that they both independently come to the
// same conclusion of what the OAuth2 shared client secret should be.
func formulateOAuthClientSecret(in []byte) string {
h := sha256.New()
_, err := h.Write(in)
if err != nil {
panic(err)
}
sha := h.Sum(nil)
return base64.URLEncoding.EncodeToString(sha)[:40]
}
// getSecretValues is a convenience to get the ArgoCD secret data as a map[string]string
func getSecretValues(kubeClientset kubernetes.Interface, namespace string) (map[string]string, error) {
sec, err := kubeClientset.CoreV1().Secrets(namespace).Get(common.ArgoCDSecretName, metav1.GetOptions{})
if err != nil {
return nil, err
}
secretValues := make(map[string]string, len(sec.Data))
for k, v := range sec.Data {
secretValues[k] = string(v)
}
return secretValues, nil
}
// replaceMapSecrets takes a json object and recursively looks for any secret key references in the
// object and replaces the value with the secret value
func replaceMapSecrets(obj map[string]interface{}, secretValues map[string]string) map[string]interface{} {
newObj := make(map[string]interface{})
for k, v := range obj {
switch val := v.(type) {
case map[string]interface{}:
newObj[k] = replaceMapSecrets(val, secretValues)
case []interface{}:
newObj[k] = replaceListSecrets(val, secretValues)
case string:
newObj[k] = replaceStringSecret(val, secretValues)
default:
newObj[k] = val
}
}
return newObj
}
func replaceListSecrets(obj []interface{}, secretValues map[string]string) []interface{} {
newObj := make([]interface{}, len(obj))
for i, v := range obj {
switch val := v.(type) {
case map[string]interface{}:
newObj[i] = replaceMapSecrets(val, secretValues)
case []interface{}:
newObj[i] = replaceListSecrets(val, secretValues)
case string:
newObj[i] = replaceStringSecret(val, secretValues)
default:
newObj[i] = val
}
}
return newObj
}
func replaceStringSecret(val string, secretValues map[string]string) string {
if val == "" || !strings.HasPrefix(val, "$") {
return val
}
secretKey := val[1:]
secretVal, ok := secretValues[secretKey]
if !ok {
log.Warnf("config referenced '%s', but key does not exist in secret", val)
return val
}
return secretVal
}
// needsRedirectURI returns whether or not the given connector type needs a redirectURI
// Update this list as necessary, as new connectors are added
// https://github.com/coreos/dex/tree/master/Documentation/connectors
func needsRedirectURI(connectorType string) bool {
switch connectorType {
case "oidc", "saml", "microsoft", "linkedin", "gitlab", "github":
return true
}
return false
}

323
util/dex/dex.go Normal file
View File

@@ -0,0 +1,323 @@
package dex
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"time"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/util/session"
"github.com/coreos/dex/api"
oidc "github.com/coreos/go-oidc"
jwt "github.com/dgrijalva/jwt-go"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"google.golang.org/grpc"
)
const (
// DexReverseProxyAddr is the address of the Dex OIDC server, which we run a reverse proxy against
DexReverseProxyAddr = "http://localhost:5556"
// DexgRPCAPIAddr is the address to the Dex gRPC API server for managing dex. This is assumed to run
// locally (as a sidecar)
DexgRPCAPIAddr = "localhost:5557"
// DexAPIEndpoint is the endpoint where we serve the Dex API server
DexAPIEndpoint = "/api/dex"
// LoginEndpoint is ArgoCD's shorthand login endpoint which redirects to dex's OAuth 2.0 provider's consent page
LoginEndpoint = "/auth/login"
// CallbackEndpoint is ArgoCD's final callback endpoint we reach after OAuth 2.0 login flow has been completed
CallbackEndpoint = "/auth/callback"
// envVarSSODebug is an environment variable to enable additional OAuth debugging in the API server
envVarSSODebug = "ARGOCD_SSO_DEBUG"
)
type DexAPIClient struct {
api.DexClient
}
// NewDexHTTPReverseProxy returns a reverse proxy to the DEX server. Dex is assumed to be configured
// with the external issuer URL muxed to the same path configured in server.go. In other words, if
// ArgoCD API server wants to proxy requests at /api/dex, then the dex config yaml issuer URL should
// also be /api/dex (e.g. issuer: https://argocd.example.com/api/dex)
func NewDexHTTPReverseProxy() func(writer http.ResponseWriter, request *http.Request) {
target, err := url.Parse(DexReverseProxyAddr)
errors.CheckError(err)
proxy := httputil.NewSingleHostReverseProxy(target)
return func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
}
}
func NewDexClient() (*DexAPIClient, error) {
conn, err := grpc.Dial(DexgRPCAPIAddr, grpc.WithInsecure())
if err != nil {
return nil, fmt.Errorf("failed to dial %s: %v", DexgRPCAPIAddr, err)
}
apiClient := DexAPIClient{
api.NewDexClient(conn),
}
return &apiClient, nil
}
// WaitUntilReady waits until the dex gRPC server is responding
func (d *DexAPIClient) WaitUntilReady() {
log.Info("Waiting for dex to become ready")
ctx := context.Background()
for {
vers, err := d.GetVersion(ctx, &api.VersionReq{})
if err == nil {
log.Infof("Dex %s (API: %d) up and running", vers.Server, vers.Api)
return
}
time.Sleep(1 * time.Second)
}
}
// TODO: implement proper state management
const exampleAppState = "I wish to wash my irish wristwatch"
type ClientApp struct {
// OAuth2 client ID of this application (e.g. argo-cd)
clientID string
// OAuth2 client secret of this application
clientSecret string
// Callback URL for OAuth2 responses (e.g. https://argocd.example.com/auth/callback)
redirectURI string
// URL of the issuer (e.g. https://argocd.example.com/api/dex)
issuerURL string
Path string
verifier *oidc.IDTokenVerifier
provider *oidc.Provider
// Does the provider use "offline_access" scope to request a refresh token
// or does it use "access_type=offline" (e.g. Google)?
offlineAsScope bool
client *http.Client
// sessionMgr creates and validates sessions
sessionMgr session.SessionManager
// secureCookie indicates if the cookie should be set with the Secure flag, meaning it should
// only ever be sent over HTTPS. This value is inferred by the scheme of the redirectURI.
secureCookie bool
}
// NewClientApp will register the ArgoCD client app in Dex and return an object which has HTTP
// handlers for handling the HTTP responses for login and callback
func NewClientApp(clientBaseURL string, serverSecretKey []byte, tlsConfig *tls.Config) (*ClientApp, error) {
redirectURI := clientBaseURL + CallbackEndpoint
issuerURL := clientBaseURL + DexAPIEndpoint
log.Infof("Creating client app (redirectURI: %s, issuerURL: %s)", redirectURI, issuerURL)
a := ClientApp{
clientID: DexClientAppID,
clientSecret: formulateOAuthClientSecret(serverSecretKey),
redirectURI: redirectURI,
issuerURL: issuerURL,
}
u, err := url.Parse(redirectURI)
if err != nil {
return nil, fmt.Errorf("parse redirect-uri: %v", err)
}
a.Path = u.Path
a.secureCookie = bool(u.Scheme == "https")
a.client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
if os.Getenv(envVarSSODebug) == "1" {
a.client.Transport = debugTransport{a.client.Transport}
}
a.sessionMgr = session.MakeSessionManager(serverSecretKey)
return &a, nil
}
type debugTransport struct {
t http.RoundTripper
}
func (d debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
reqDump, err := httputil.DumpRequest(req, true)
if err != nil {
return nil, err
}
log.Printf("%s", reqDump)
resp, err := d.t.RoundTrip(req)
if err != nil {
return nil, err
}
respDump, err := httputil.DumpResponse(resp, true)
if err != nil {
_ = resp.Body.Close()
return nil, err
}
log.Printf("%s", respDump)
return resp, nil
}
// Initialize initializes the client app. The OIDC provider must be running
func (a *ClientApp) Initialize() error {
log.Info("Initializing client app")
ctx := oidc.ClientContext(context.Background(), a.client)
provider, err := oidc.NewProvider(ctx, a.issuerURL)
if err != nil {
return fmt.Errorf("Failed to query provider %q: %v", a.issuerURL, err)
}
var s struct {
// What scopes does a provider support?
// See: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
ScopesSupported []string `json:"scopes_supported"`
}
if err := provider.Claims(&s); err != nil {
return fmt.Errorf("Failed to parse provider scopes_supported: %v", err)
}
log.Infof("OpenID supported scopes: %v", s.ScopesSupported)
a.provider = provider
a.verifier = provider.Verifier(&oidc.Config{ClientID: a.clientID})
if len(s.ScopesSupported) == 0 {
// scopes_supported is a "RECOMMENDED" discovery claim, not a required
// one. If missing, assume that the provider follows the spec and has
// an "offline_access" scope.
a.offlineAsScope = true
} else {
// See if scopes_supported has the "offline_access" scope.
a.offlineAsScope = func() bool {
for _, scope := range s.ScopesSupported {
if scope == oidc.ScopeOfflineAccess {
return true
}
}
return false
}()
}
return nil
}
func (a *ClientApp) oauth2Config(scopes []string) *oauth2.Config {
return &oauth2.Config{
ClientID: a.clientID,
ClientSecret: a.clientSecret,
Endpoint: a.provider.Endpoint(),
Scopes: scopes,
RedirectURL: a.redirectURI,
}
}
func (a *ClientApp) HandleLogin(w http.ResponseWriter, r *http.Request) {
var authCodeURL string
scopes := []string{"openid", "profile", "email", "groups"}
if r.FormValue("offline_access") != "yes" {
authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState)
} else if a.offlineAsScope {
scopes = append(scopes, "offline_access")
authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState)
} else {
authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState, oauth2.AccessTypeOffline)
}
http.Redirect(w, r, authCodeURL, http.StatusSeeOther)
}
func (a *ClientApp) HandleCallback(w http.ResponseWriter, r *http.Request) {
var (
err error
token *oauth2.Token
)
ctx := oidc.ClientContext(r.Context(), a.client)
oauth2Config := a.oauth2Config(nil)
switch r.Method {
case "GET":
// Authorization redirect callback from OAuth2 auth flow.
if errMsg := r.FormValue("error"); errMsg != "" {
http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest)
return
}
code := r.FormValue("code")
if code == "" {
http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest)
return
}
if state := r.FormValue("state"); state != exampleAppState {
http.Error(w, fmt.Sprintf("expected state %q got %q", exampleAppState, state), http.StatusBadRequest)
return
}
token, err = oauth2Config.Exchange(ctx, code)
case "POST":
// Form request from frontend to refresh a token.
refresh := r.FormValue("refresh_token")
if refresh == "" {
http.Error(w, fmt.Sprintf("no refresh_token in request: %q", r.Form), http.StatusBadRequest)
return
}
t := &oauth2.Token{
RefreshToken: refresh,
Expiry: time.Now().Add(-time.Hour),
}
token, err = oauth2Config.TokenSource(ctx, t).Token()
default:
http.Error(w, fmt.Sprintf("method not implemented: %s", r.Method), http.StatusBadRequest)
return
}
if err != nil {
http.Error(w, fmt.Sprintf("failed to get token: %v", err), http.StatusInternalServerError)
return
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
http.Error(w, "no id_token in token response", http.StatusInternalServerError)
return
}
idToken, err := a.verifier.Verify(r.Context(), rawIDToken)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to verify ID token: %v", err), http.StatusInternalServerError)
return
}
var claims jwt.MapClaims
err = idToken.Claims(&claims)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to unmarshal claims: %v", err), http.StatusInternalServerError)
return
}
clientToken, err := a.sessionMgr.SignClaims(claims)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to sign identity provider claims: %v", err), http.StatusInternalServerError)
return
}
flags := []string{"path=/"}
if a.secureCookie {
flags = append(flags, "Secure")
}
cookie := session.MakeCookieMetadata(common.AuthCookieName, clientToken, flags...)
w.Header().Set("Set-Cookie", cookie)
if os.Getenv(envVarSSODebug) == "1" {
claimsJSON, _ := json.MarshalIndent(claims, "", " ")
renderToken(w, a.redirectURI, rawIDToken, token.RefreshToken, claimsJSON)
} else {
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}

69
util/dex/templates.go Normal file
View File

@@ -0,0 +1,69 @@
package dex
import (
"html/template"
"log"
"net/http"
)
type tokenTmplData struct {
IDToken string
RefreshToken string
RedirectURL string
Claims string
}
var tokenTmpl = template.Must(template.New("token.html").Parse(`<html>
<head>
<style>
/* make pre wrap */
pre {
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
</style>
</head>
<body>
<p> Token: <pre><code>{{ .IDToken }}</code></pre></p>
<p> Claims: <pre><code>{{ .Claims }}</code></pre></p>
{{ if .RefreshToken }}
<p> Refresh Token: <pre><code>{{ .RefreshToken }}</code></pre></p>
<form action="{{ .RedirectURL }}" method="post">
<input type="hidden" name="refresh_token" value="{{ .RefreshToken }}">
<input type="submit" value="Redeem refresh token">
</form>
{{ end }}
</body>
</html>
`))
func renderToken(w http.ResponseWriter, redirectURL, idToken, refreshToken string, claims []byte) {
renderTemplate(w, tokenTmpl, tokenTmplData{
IDToken: idToken,
RefreshToken: refreshToken,
RedirectURL: redirectURL,
Claims: string(claims),
})
}
func renderTemplate(w http.ResponseWriter, tmpl *template.Template, data interface{}) {
err := tmpl.Execute(w, data)
if err == nil {
return
}
switch err := err.(type) {
case *template.Error:
// An ExecError guarantees that Execute has not written to the underlying reader.
log.Printf("Error rendering template %s: %s", tmpl.Name(), err)
// TODO(ericchiang): replace with better internal server error.
http.Error(w, "Internal server error", http.StatusInternalServerError)
default:
// An error with the underlying write, such as the connection being
// dropped. Ignore for now.
}
}

View File

@@ -43,6 +43,7 @@ func (m *NativeGitClient) Init(repo string, repoPath string) error {
// SetCredentials sets a local credentials file to connect to a remote git repository
func (m *NativeGitClient) SetCredentials(repo string, username string, password string, sshPrivateKey string, repoPath string) error {
if password != "" {
log.Infof("Setting password credentials")
gitCredentialsFile := path.Join(repoPath, ".git", "credentials")
repoURL, err := url.ParseRequestURI(repo)
if err != nil {
@@ -60,6 +61,7 @@ func (m *NativeGitClient) SetCredentials(repo string, username string, password
}
}
if sshPrivateKey != "" {
log.Infof("Setting SSH credentials")
sshPrivateKeyFile := path.Join(repoPath, ".git", "ssh-private-key")
err := ioutil.WriteFile(sshPrivateKeyFile, []byte(sshPrivateKey), 0600)
if err != nil {

View File

@@ -2,7 +2,9 @@ package session
import (
"crypto/rand"
"encoding/base64"
"fmt"
"strings"
"time"
util_password "github.com/argoproj/argo-cd/util/password"
@@ -23,12 +25,6 @@ const (
blankPasswordError = "Blank passwords are not allowed"
)
// SessionManagerTokenClaims holds claim metadata for a token.
type SessionManagerTokenClaims struct {
//Foo string `json:"foo"`
jwt.StandardClaims
}
// MakeSessionManager creates a new session manager with the given secret key.
func MakeSessionManager(secretKey []byte) SessionManager {
return SessionManager{
@@ -41,43 +37,39 @@ func (mgr SessionManager) Create(subject string) (string, error) {
// Create a new token object, specifying signing method and the claims
// you would like it to contain.
now := time.Now().Unix()
claims := SessionManagerTokenClaims{
//"bar",
jwt.StandardClaims{
//ExpiresAt: time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
IssuedAt: now,
Issuer: sessionManagerClaimsIssuer,
NotBefore: now,
Subject: subject,
},
claims := jwt.StandardClaims{
//ExpiresAt: time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
IssuedAt: now,
Issuer: sessionManagerClaimsIssuer,
NotBefore: now,
Subject: subject,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return mgr.SignClaims(claims)
}
// Unix and get the complete encoded token as a string using the secret
func (mgr SessionManager) SignClaims(claims jwt.Claims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(mgr.serverSecretKey)
}
// Parse tries to parse the provided string and returns the token claims.
func (mgr SessionManager) Parse(tokenString string) (*SessionManagerTokenClaims, error) {
func (mgr SessionManager) Parse(tokenString string) (jwt.Claims, error) {
// Parse takes the token string and a function for looking up the key. The latter is especially
// useful if you use multiple keys for your application. The standard is to use 'kid' in the
// head of the token to identify which key to use, but the parsed token (head and claims) is provided
// to the callback, providing flexibility.
token, err := jwt.ParseWithClaims(tokenString, &SessionManagerTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
var claims jwt.MapClaims
token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return mgr.serverSecretKey, nil
})
if token != nil {
if claims, ok := token.Claims.(*SessionManagerTokenClaims); ok && token.Valid {
return claims, nil
}
if err != nil {
return nil, err
}
return nil, err
return token.Claims, nil
}
// MakeSignature generates a cryptographically-secure pseudo-random token, based on a given number of random bytes, for signing purposes.
@@ -87,6 +79,8 @@ func MakeSignature(size int) ([]byte, error) {
if err != nil {
b = nil
}
// base64 encode it so signing key can be typed into validation utilities
b = []byte(base64.StdEncoding.EncodeToString(b))
return b, err
}
@@ -116,3 +110,12 @@ func (mgr SessionManager) LoginLocalUser(username, password string, users map[st
return "", fmt.Errorf(invalidLoginError)
}
// MakeCookieMetadata generates a string representing a Web cookie. Yum!
func MakeCookieMetadata(key, value string, flags ...string) string {
components := []string{
fmt.Sprintf("%s=%s", key, value),
}
components = append(components, flags...)
return strings.Join(components, "; ")
}

View File

@@ -2,6 +2,8 @@ package session
import (
"testing"
jwt "github.com/dgrijalva/jwt-go"
)
func TestSessionManager(t *testing.T) {
@@ -21,7 +23,8 @@ func TestSessionManager(t *testing.T) {
t.Errorf("Could not parse token: %v", err)
}
subject := claims.Subject
mapClaims := *(claims.(*jwt.MapClaims))
subject := mapClaims["sub"].(string)
if subject != "argo" {
t.Errorf("Token claim subject \"%s\" does not match expected subject \"%s\".", subject, defaultSubject)
}

182
util/settings/settings.go Normal file
View File

@@ -0,0 +1,182 @@
package settings
import (
"crypto/tls"
"fmt"
"github.com/argoproj/argo-cd/common"
tlsutil "github.com/argoproj/argo-cd/util/tls"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
apiv1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// ArgoCDSettings holds in-memory runtime configuration options.
type ArgoCDSettings struct {
// URL is the externally facing URL users will visit to reach ArgoCD.
// The value here is used when configuring SSO. Omitting this value will disable SSO.
URL string
// DexConfig is contains portions of a dex config yaml
DexConfig string
// LocalUsers holds users local to (stored on) the server. This is to be distinguished from any potential alternative future login providers (LDAP, SAML, etc.) that might ever be added.
LocalUsers map[string]string
// ServerSignature holds the key used to generate JWT tokens.
ServerSignature []byte
// Certificate holds the certificate/private key for the ArgoCD API server.
// If nil, will run insecure without TLS.
Certificate *tls.Certificate
}
const (
// settingAdminPasswordKey designates the key for a root password inside a Kubernetes secret.
settingAdminPasswordKey = "admin.password"
// settingServerSignatureKey designates the key for a server secret key inside a Kubernetes secret.
settingServerSignatureKey = "server.secretkey"
// settingServerCertificate designates the key for the public cert used in TLS
settingServerCertificate = "server.crt"
// settingServerPrivateKey designates the key for the private key used in TLS
settingServerPrivateKey = "server.key"
// settingURLKey designates the key where ArgoCDs external URL is set
settingURLKey = "url"
// settingDexConfigKey designates the key for the dex config
settingDexConfigKey = "dex.config"
)
// SettingsManager holds config info for a new manager with which to access Kubernetes ConfigMaps.
type SettingsManager struct {
clientset kubernetes.Interface
namespace string
}
// GetSettings retrieves settings from the ConfigManager.
func (mgr *SettingsManager) GetSettings() (*ArgoCDSettings, error) {
argoCDCM, err := mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Get(common.ArgoCDConfigMapName, metav1.GetOptions{})
if err != nil {
return nil, err
}
argoCDSecret, err := mgr.clientset.CoreV1().Secrets(mgr.namespace).Get(common.ArgoCDSecretName, metav1.GetOptions{})
if err != nil {
return nil, err
}
var settings ArgoCDSettings
settings.DexConfig = argoCDCM.Data[settingDexConfigKey]
settings.URL = argoCDCM.Data[settingURLKey]
adminPasswordHash, ok := argoCDSecret.Data[settingAdminPasswordKey]
if !ok {
return nil, fmt.Errorf("admin user not found")
}
settings.LocalUsers = map[string]string{
common.ArgoCDAdminUsername: string(adminPasswordHash),
}
secretKey, ok := argoCDSecret.Data[settingServerSignatureKey]
if !ok {
return nil, fmt.Errorf("server secret key not found")
}
settings.ServerSignature = secretKey
serverCert, certOk := argoCDSecret.Data[settingServerCertificate]
serverKey, keyOk := argoCDSecret.Data[settingServerPrivateKey]
if certOk && keyOk {
cert, err := tls.X509KeyPair(serverCert, serverKey)
if err != nil {
return nil, fmt.Errorf("invalid x509 key pair %s/%s in secret: %s", settingServerCertificate, settingServerPrivateKey, err)
}
settings.Certificate = &cert
}
return &settings, nil
}
// SaveSettings serializes ArgoCD settings and upserts it into K8s secret/configmap
func (mgr *SettingsManager) SaveSettings(settings *ArgoCDSettings) error {
// Upsert the config data
argoCDCM, err := mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Get(common.ArgoCDConfigMapName, metav1.GetOptions{})
createCM := false
if err != nil {
if !apierr.IsNotFound(err) {
return err
}
argoCDCM = &apiv1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: common.ArgoCDConfigMapName,
},
Data: make(map[string]string),
}
createCM = true
}
argoCDCM.Data[settingURLKey] = settings.URL
argoCDCM.Data[settingDexConfigKey] = settings.DexConfig
if createCM {
_, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Create(argoCDCM)
} else {
_, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Update(argoCDCM)
}
if err != nil {
return err
}
// Upsert the secret data. Ensure we do not delete any extra keys which user may have added
argoCDSecret, err := mgr.clientset.CoreV1().Secrets(mgr.namespace).Get(common.ArgoCDSecretName, metav1.GetOptions{})
createSecret := false
if err != nil {
if !apierr.IsNotFound(err) {
return err
}
argoCDSecret = &apiv1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: common.ArgoCDSecretName,
},
Data: make(map[string][]byte),
}
createSecret = true
}
argoCDSecret.StringData = make(map[string]string)
argoCDSecret.StringData[settingServerSignatureKey] = string(settings.ServerSignature)
argoCDSecret.StringData[settingAdminPasswordKey] = settings.LocalUsers[common.ArgoCDAdminUsername]
if settings.Certificate != nil {
certBytes, keyBytes := tlsutil.EncodeX509KeyPair(*settings.Certificate)
argoCDSecret.StringData[settingServerCertificate] = string(certBytes)
argoCDSecret.StringData[settingServerPrivateKey] = string(keyBytes)
} else {
delete(argoCDSecret.Data, settingServerCertificate)
delete(argoCDSecret.Data, settingServerPrivateKey)
}
if createSecret {
_, err = mgr.clientset.CoreV1().Secrets(mgr.namespace).Create(argoCDSecret)
} else {
_, err = mgr.clientset.CoreV1().Secrets(mgr.namespace).Update(argoCDSecret)
}
if err != nil {
return err
}
return nil
}
// NewSettingsManager generates a new SettingsManager pointer and returns it
func NewSettingsManager(clientset kubernetes.Interface, namespace string) *SettingsManager {
return &SettingsManager{
clientset: clientset,
namespace: namespace,
}
}
func (a *ArgoCDSettings) IsSSOConfigured() bool {
if a.URL == "" {
return false
}
var dexCfg map[string]interface{}
err := yaml.Unmarshal([]byte(a.DexConfig), &dexCfg)
if err != nil {
log.Warn("invalid dex yaml config")
return false
}
return len(dexCfg) > 0
}

155
workflows/bluegreen.yaml Normal file
View File

@@ -0,0 +1,155 @@
# This example demonstrates a "blue-green" deployment
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: k8s-bluegreen-
spec:
entrypoint: k8s-bluegreen
arguments:
parameters:
- name: deployment-name
- name: service-name
- name: new-deployment-manifest
templates:
- name: k8s-bluegreen
steps:
# 1. Create a parallel Kubernetes deployment with tweaks to name and app name
- - name: create-blue-deployment
template: clone-deployment
arguments:
parameters:
- name: green-deployment-name
value: '{{workflow.parameters.deployment-name}}'
- name: suffix
value: blue
# 2. Wait for parallel deployment to become ready
- - name: wait-for-blue-deployment
template: wait-deployment-ready
arguments:
parameters:
- name: deployment-name
value: '{{steps.create-blue-deployment.outputs.parameters.blue-deployment-name}}'
# 3. Patch the named service to point to the parallel deployment app
- - name: switch-service-to-blue-deployment
template: patch-service
arguments:
parameters:
- name: service-name
value: '{{workflow.parameters.service-name}}'
- name: app-name
value: '{{steps.create-blue-deployment.outputs.parameters.blue-deployment-app-name}}'
# 4. Update the original deployment (receiving no traffic) with a new version
- - name: create-green-deployment
template: patch-deployment
arguments:
parameters:
- name: deployment-manifest-data
value: '{{workflow.parameters.new-deployment-manifest}}'
# 5. Wait for the original deployment, now updated, to become ready
- - name: wait-for-green-deployment
template: wait-deployment-ready
arguments:
parameters:
- name: deployment-name
value: '{{steps.create-green-deployment.outputs.parameters.green-deployment-name}}'
# 6. Patch the named service to point to the original, now updated app
- - name: switch-service-to-green-deployment
template: patch-service
arguments:
parameters:
- name: service-name
value: '{{workflow.parameters.service-name}}'
- name: app-name
value: '{{steps.create-green-deployment.outputs.parameters.green-deployment-app-name}}'
# 7. Remove the cloned deployment (no longer receiving traffic)
- - name: delete-cloned-deployment
template: delete-deployment
arguments:
parameters:
- name: deployment-name
value: '{{steps.create-blue-deployment.outputs.parameters.blue-deployment-name}}'
# end of steps
- name: clone-deployment
inputs:
parameters:
- name: green-deployment-name
- name: suffix
container:
image: argoproj/argoexec:latest
command: [sh, -c]
args: ["
kubectl get -o json deployments/{{inputs.parameters.green-deployment-name}} | jq -c '.metadata.name+=\"-{{inputs.parameters.suffix}}\" | (.metadata.labels.app, .spec.selector.matchLabels.app, .spec.template.metadata.labels.app) +=\"-{{inputs.parameters.suffix}}\"' | kubectl create -o json -f - > /tmp/blue-deployment;
jq -j .metadata.name /tmp/blue-deployment > /tmp/blue-deployment-name;
jq -j .spec.template.metadata.labels.app /tmp/blue-deployment > /tmp/blue-deployment-app-name
"]
outputs:
parameters:
- name: blue-deployment-name
valueFrom:
path: /tmp/blue-deployment-name
- name: blue-deployment-app-name
valueFrom:
path: /tmp/blue-deployment-app-name
- name: patch-deployment
inputs:
parameters:
- name: deployment-manifest-data
container:
image: argoproj/argoexec:latest
command: [sh, -c]
args: ["
echo '{{inputs.parameters.deployment-manifest-data}}' | kubectl apply -o json -f - > /tmp/green-deployment;
jq -j .metadata.name /tmp/green-deployment > /tmp/green-deployment-name;
jq -j .spec.template.metadata.labels.app /tmp/green-deployment > /tmp/green-deployment-app-name
"]
outputs:
parameters:
- name: green-deployment-name
valueFrom:
path: /tmp/green-deployment-name
- name: green-deployment-app-name
valueFrom:
path: /tmp/green-deployment-app-name
- name: wait-deployment-ready
inputs:
parameters:
- name: deployment-name
container:
image: argoproj/argoexec:latest
command: [sh, -c]
args: ["kubectl rollout status --watch=true 'deployments/{{inputs.parameters.deployment-name}}'"]
- name: patch-service
inputs:
parameters:
- name: service-name
- name: app-name
container:
image: argoproj/argoexec:latest
command: [sh, -c]
args: ["kubectl patch service '{{inputs.parameters.service-name}}' -p '{\"spec\": {\"selector\": {\"app\": \"{{inputs.parameters.app-name}}\"}}}'"]
- name: delete-deployment
inputs:
parameters:
- name: deployment-name
resource:
action: delete
manifest: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{inputs.parameters.deployment-name}}