argo-cd/server/server.go
Andrew Merenbach ab00aef75e
Generate swagger files (#278)
* Generate swagger files

* Add basic Swagger definitions

* Add reposerver swagger file

* Consolidate swagger files

* Move swagger files to swagger-ui directory instead

* Put swagger files in swagger-ui

* Fix order of operations

* Move back to swagger directory

* Serve API server swagger files raw for now

* Serve reposerver swagger files from API server

* Move back to subdirectories, thanks @alexmt

* Fix comment on application Rollback

* Update two more comments

* Fix comment in session.proto

* Update generated code

* Update generated swagger docs

* Fix comment for delete actions in cluster and repository swagger

* Set expected collisions and invoke mixins

* Update generated code

* Create swagger mixins from codegen

* Move swagger.json location, thanks @jazminGonzalez-Rivero

* Add ref cleanup for swagger combined

* Make fewer temp files when generating swagger

* Delete intermediate swagger files

* Serve new file at /swagger.json

* Set up UI server

* Update package lock

* Commit generated swagger.json files

* Add install commands for swagger

* Use ReDoc server instead of Swagger UI server

* Update lockfile

* Make URL paths more consistent

* Update package lock

* Separate out handlers for Swagger UI, JSON

* Rm unnecessary CORS headers

...since we're serving from the app server

* Simplify serving

* Further simplify serving code

* Update package lock

* Factor out swagger serving into util

* Add test for Swagger server

* Use ServeSwaggerUI method to run tests

* Update package lock

* Don't generate swagger for reposerver

* Reset to master Gopkg.lock and server/server.go

* Merge in prev change to server/server.go

* Redo changes to Gopkg.lock

* Fix number of conflicts

* Update generated swagger.json for server

* Fix issue with project feature error
2018-06-25 13:49:38 -07:00

559 lines
19 KiB
Go

package server
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/gobuffalo/packr"
golang_proto "github.com/golang/protobuf/proto"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
log "github.com/sirupsen/logrus"
"github.com/soheilhy/cmux"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"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"
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/project"
"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"
"github.com/argoproj/argo-cd/util/db"
"github.com/argoproj/argo-cd/util/dex"
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"
"github.com/argoproj/argo-cd/util/rbac"
util_session "github.com/argoproj/argo-cd/util/session"
settings_util "github.com/argoproj/argo-cd/util/settings"
"github.com/argoproj/argo-cd/util/swagger"
tlsutil "github.com/argoproj/argo-cd/util/tls"
"github.com/argoproj/argo-cd/util/webhook"
)
var (
// ErrNoSession indicates no auth token was supplied as part of a request
ErrNoSession = status.Errorf(codes.Unauthenticated, "no session information")
)
var backoff = wait.Backoff{
Steps: 5,
Duration: 500 * time.Millisecond,
Factor: 1.0,
Jitter: 0.1,
}
var (
box = packr.NewBox("../util/rbac")
builtinPolicy string
)
func init() {
var err error
builtinPolicy, err = box.MustString("builtin-policy.csv")
errors.CheckError(err)
}
// ArgoCDServer is the API server for ArgoCD
type ArgoCDServer struct {
ArgoCDServerOpts
ssoClientApp *dexutil.ClientApp
settings *settings_util.ArgoCDSettings
log *log.Entry
sessionMgr *util_session.SessionManager
settingsMgr *settings_util.SettingsManager
enf *rbac.Enforcer
// stopCh is the channel which when closed, will shutdown the ArgoCD server
stopCh chan struct{}
}
type ArgoCDServerOpts struct {
DisableAuth bool
Insecure bool
Namespace string
StaticAssetsDir string
KubeClientset kubernetes.Interface
AppClientset appclientset.Interface
RepoClientset reposerver.Clientset
}
// NewServer returns a new instance of the ArgoCD API server
func NewServer(opts ArgoCDServerOpts) *ArgoCDServer {
settingsMgr := settings_util.NewSettingsManager(opts.KubeClientset, opts.Namespace)
settings, err := settingsMgr.GetSettings()
errors.CheckError(err)
sessionMgr := util_session.NewSessionManager(settings)
enf := rbac.NewEnforcer(opts.KubeClientset, opts.Namespace, common.ArgoCDRBACConfigMapName, nil)
enf.EnableEnforce(!opts.DisableAuth)
err = enf.SetBuiltinPolicy(builtinPolicy)
errors.CheckError(err)
enf.EnableLog(os.Getenv(common.EnvVarRBACDebug) == "1")
return &ArgoCDServer{
ArgoCDServerOpts: opts,
log: log.NewEntry(log.New()),
settings: settings,
sessionMgr: sessionMgr,
settingsMgr: settingsMgr,
enf: enf,
}
}
// Run runs the API Server
// We use k8s.io/code-generator/cmd/go-to-protobuf to generate the .proto files from the API types.
// k8s.io/ go-to-protobuf uses protoc-gen-gogo, which comes from gogo/protobuf (a fork of
// golang/protobuf).
func (a *ArgoCDServer) Run(ctx context.Context, port int) {
grpcS := a.newGRPCServer()
var httpS *http.Server
var httpsS *http.Server
if a.useTLS() {
httpS = newRedirectServer(port)
httpsS = a.newHTTPServer(ctx, port)
} else {
httpS = a.newHTTPServer(ctx, port)
}
// Start listener
var conn net.Listener
var realErr error
_ = wait.ExponentialBackoff(backoff, func() (bool, error) {
conn, realErr = net.Listen("tcp", fmt.Sprintf(":%d", port))
if realErr != nil {
a.log.Warnf("failed listen: %v", realErr)
return false, nil
}
return true, nil
})
errors.CheckError(realErr)
// Cmux is used to support servicing gRPC and HTTP1.1+JSON on the same port
tcpm := cmux.New(conn)
var tlsm cmux.CMux
var grpcL net.Listener
var httpL net.Listener
var httpsL net.Listener
if !a.useTLS() {
httpL = tcpm.Match(cmux.HTTP1Fast())
grpcL = tcpm.Match(cmux.HTTP2HeaderField("content-type", "application/grpc"))
} else {
// We first match on HTTP 1.1 methods.
httpL = tcpm.Match(cmux.HTTP1Fast())
// If not matched, we assume that its TLS.
tlsl := tcpm.Match(cmux.Any())
tlsConfig := tls.Config{
Certificates: []tls.Certificate{*a.settings.Certificate},
}
tlsl = tls.NewListener(tlsl, &tlsConfig)
// 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 (url: %s, tls: %v, namespace: %s, sso: %v)",
argocd.GetVersion(), port, a.settings.URL, a.useTLS(), a.Namespace, a.settings.IsSSOConfigured())
go func() { a.checkServeErr("grpcS", grpcS.Serve(grpcL)) }()
go func() { a.checkServeErr("httpS", httpS.Serve(httpL)) }()
if a.useTLS() {
go func() { a.checkServeErr("httpsS", httpsS.Serve(httpsL)) }()
go func() { a.checkServeErr("tlsm", tlsm.Serve()) }()
}
go a.watchSettings(ctx)
go a.rbacPolicyLoader(ctx)
go func() { a.checkServeErr("tcpm", tcpm.Serve()) }()
a.stopCh = make(chan struct{})
<-a.stopCh
errors.CheckError(conn.Close())
}
// checkServeErr checks the error from a .Serve() call to decide if it was a graceful shutdown
func (a *ArgoCDServer) checkServeErr(name string, err error) {
if err != nil {
if a.stopCh == nil {
// a nil stopCh indicates a graceful shutdown
log.Infof("graceful shutdown %s: %v", name, err)
} else {
log.Fatalf("%s: %v", name, err)
}
} else {
log.Infof("graceful shutdown %s", name)
}
}
func (a *ArgoCDServer) Shutdown() {
log.Info("Shut down requested")
stopCh := a.stopCh
a.stopCh = nil
if stopCh != nil {
close(stopCh)
}
}
// watchSettings watches the configmap and secret for any setting updates that would warrant a
// restart of the API server.
func (a *ArgoCDServer) watchSettings(ctx context.Context) {
a.settingsMgr.StartNotifier(ctx, a.settings)
updateCh := make(chan struct{}, 1)
a.settingsMgr.Subscribe(updateCh)
prevDexCfgBytes, err := dex.GenerateDexConfigYAML(a.settings)
errors.CheckError(err)
prevGitHubSecret := a.settings.WebhookGitHubSecret
prevGitLabSecret := a.settings.WebhookGitLabSecret
prevBitBucketUUID := a.settings.WebhookBitbucketUUID
var prevCert, prevCertKey string
if a.settings.Certificate != nil {
prevCert, prevCertKey = tlsutil.EncodeX509KeyPairString(*a.settings.Certificate)
}
for {
<-updateCh
newDexCfgBytes, err := dex.GenerateDexConfigYAML(a.settings)
errors.CheckError(err)
if string(newDexCfgBytes) != string(prevDexCfgBytes) {
log.Infof("dex config modified. restarting")
break
}
if prevGitHubSecret != a.settings.WebhookGitHubSecret {
log.Infof("github secret modified. restarting")
break
}
if prevGitLabSecret != a.settings.WebhookGitLabSecret {
log.Infof("gitlab secret modified. restarting")
break
}
if prevBitBucketUUID != a.settings.WebhookBitbucketUUID {
log.Infof("bitbucket uuid modified. restarting")
break
}
var newCert, newCertKey string
if a.settings.Certificate != nil {
newCert, newCertKey = tlsutil.EncodeX509KeyPairString(*a.settings.Certificate)
}
if newCert != prevCert || newCertKey != prevCertKey {
log.Infof("tls certificate modified. restarting")
break
}
}
log.Info("shutting down settings watch")
a.Shutdown()
a.settingsMgr.Unsubscribe(updateCh)
close(updateCh)
}
func (a *ArgoCDServer) rbacPolicyLoader(ctx context.Context) {
err := a.enf.RunPolicyLoader(ctx)
errors.CheckError(err)
}
func (a *ArgoCDServer) useTLS() bool {
if a.Insecure || a.settings.Certificate == nil {
return false
}
return true
}
func (a *ArgoCDServer) newGRPCServer() *grpc.Server {
var sOpts []grpc.ServerOption
// NOTE: notice we do not configure the gRPC server here with TLS (e.g. grpc.Creds(creds))
// This is because TLS handshaking occurs in cmux handling
sOpts = append(sOpts, grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
grpc_logrus.StreamServerInterceptor(a.log),
grpc_auth.StreamServerInterceptor(a.authenticate),
grpc_util.ErrorCodeStreamServerInterceptor(),
grpc_util.PanicLoggerStreamServerInterceptor(a.log),
)))
sOpts = append(sOpts, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
bug21955WorkaroundInterceptor,
grpc_logrus.UnaryServerInterceptor(a.log),
grpc_auth.UnaryServerInterceptor(a.authenticate),
grpc_util.ErrorCodeUnaryServerInterceptor(),
grpc_util.PanicLoggerUnaryServerInterceptor(a.log),
)))
grpcS := grpc.NewServer(sOpts...)
db := db.NewDB(a.Namespace, a.KubeClientset)
clusterService := cluster.NewServer(db, a.enf)
repoService := repository.NewServer(a.RepoClientset, db, a.enf)
sessionService := session.NewServer(a.sessionMgr)
projectLock := util.NewKeyLock()
applicationService := application.NewServer(a.Namespace, a.KubeClientset, a.AppClientset, a.RepoClientset, db, a.enf, projectLock)
projectService := project.NewServer(a.Namespace, a.AppClientset, a.enf, projectLock)
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)
project.RegisterProjectServiceServer(grpcS, projectService)
// Register reflection service on gRPC server.
reflection.Register(grpcS)
return grpcS
}
// 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 {
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
}
// newHTTPServer returns the HTTP server to serve HTTP/HTTPS requests. This is implemented
// using grpc-gateway as a proxy to the gRPC server.
func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int) *http.Server {
endpoint := fmt.Sprintf("localhost:%d", port)
mux := http.NewServeMux()
httpS := http.Server{
Addr: endpoint,
Handler: &bug21955Workaround{handler: mux},
}
var dOpts []grpc.DialOption
if a.useTLS() {
// The following sets up the dial Options for grpc-gateway to talk to gRPC server over TLS.
// 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.
tlsConfig := a.settings.TLSConfig()
tlsConfig.InsecureSkipVerify = true
dCreds := credentials.NewTLS(tlsConfig)
dOpts = append(dOpts, grpc.WithTransportCredentials(dCreds))
} else {
dOpts = append(dOpts, grpc.WithInsecure())
}
// HTTP 1.1+JSON Server
// grpc-ecosystem/grpc-gateway is used to proxy HTTP requests to the corresponding gRPC call
// NOTE: if a marshaller option is not supplied, grpc-gateway will default to the jsonpb from
// golang/protobuf. Which does not support types such as time.Time. gogo/protobuf does support
// time.Time, but does not support custom UnmarshalJSON() and MarshalJSON() methods. Therefore
// we use our own Marshaler
gwMuxOpts := runtime.WithMarshalerOption(runtime.MIMEWildcard, new(jsonutil.JSONMarshaler))
gwCookieOpts := runtime.WithForwardResponseOption(a.translateGrpcCookieHeader)
gwmux := runtime.NewServeMux(gwMuxOpts, gwCookieOpts)
mux.Handle("/api/", gwmux)
mustRegisterGWHandler(version.RegisterVersionServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
mustRegisterGWHandler(cluster.RegisterClusterServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
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)
mustRegisterGWHandler(project.RegisterProjectServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
swagger.ServeSwaggerUI(mux, "server", "/swagger-ui")
// Dex reverse proxy and client app and OAuth2 login/callback
a.registerDexHandlers(mux)
// Webhook handler for git events
acdWebhookHandler := webhook.NewHandler(a.Namespace, a.AppClientset, a.settings)
mux.HandleFunc("/api/webhook", acdWebhookHandler.Handler)
if a.StaticAssetsDir != "" {
mux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
acceptHTML := false
for _, acceptType := range strings.Split(request.Header.Get("Accept"), ",") {
if acceptType == "text/html" || acceptType == "html" {
acceptHTML = true
break
}
}
fileRequest := request.URL.Path != "/index.html" && strings.Contains(request.URL.Path, ".")
// serve index.html for non file requests to support HTML5 History API
if acceptHTML && !fileRequest && (request.Method == "GET" || request.Method == "HEAD") {
http.ServeFile(writer, request, a.StaticAssetsDir+"/index.html")
} else {
http.ServeFile(writer, request, a.StaticAssetsDir+request.URL.Path)
}
})
}
return &httpS
}
// 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(common.DexAPIEndpoint+"/", dexutil.NewDexHTTPReverseProxy())
tlsConfig := a.settings.TLSConfig()
tlsConfig.InsecureSkipVerify = true
a.ssoClientApp, err = dexutil.NewClientApp(a.settings, a.sessionMgr)
errors.CheckError(err)
mux.HandleFunc(common.LoginEndpoint, a.ssoClientApp.HandleLogin)
mux.HandleFunc(common.CallbackEndpoint, a.ssoClientApp.HandleCallback)
}
// newRedirectServer returns an HTTP server which does a 307 redirect to the HTTPS server
func newRedirectServer(port int) *http.Server {
return &http.Server{
Addr: fmt.Sprintf("localhost:%d", port),
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
target := "https://" + req.Host + req.URL.Path
if len(req.URL.RawQuery) > 0 {
target += "?" + req.URL.RawQuery
}
http.Redirect(w, req, target, http.StatusTemporaryRedirect)
}),
}
}
type registerFunc func(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error
// mustRegisterGWHandler is a convenience function to register a gateway handler
func mustRegisterGWHandler(register registerFunc, ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) {
err := register(ctx, mux, endpoint, opts)
if err != nil {
panic(err)
}
}
// 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
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ctx, ErrNoSession
}
tokenString := getToken(md)
if tokenString == "" {
return ctx, ErrNoSession
}
claims, err := a.sessionMgr.VerifyToken(tokenString)
if err != nil {
return ctx, status.Errorf(codes.Unauthenticated, "invalid session: %v", err)
}
// Add claims to the context to inspect for RBAC
ctx = context.WithValue(ctx, "claims", claims)
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"] {
header := http.Header{}
header.Add("Cookie", cookieToken)
request := http.Request{Header: header}
token, err := request.Cookie(common.AuthCookieName)
if err == nil {
return token.Value
}
}
return ""
}
// Workaround for https://github.com/golang/go/issues/21955 to support escaped URLs in URL path.
type bug21955Workaround struct {
handler http.Handler
}
func (bf *bug21955Workaround) ServeHTTP(w http.ResponseWriter, r *http.Request) {
paths := map[string][]string{
"/api/v1/repositories/": {"ksonnet"},
"/api/v1/clusters/": {},
}
for path, subPaths := range paths {
if strings.Index(r.URL.Path, path) > -1 {
postfix := ""
for _, subPath := range subPaths {
if strings.LastIndex(r.URL.Path, subPath) == len(r.URL.Path)-len(subPath) {
postfix = "/" + subPath
r.URL.Path = r.URL.Path[0 : len(r.URL.Path)-len(subPath)-1]
break
}
}
r.URL.Path = path + url.QueryEscape(r.URL.Path[len(path):]) + postfix
break
}
}
bf.handler.ServeHTTP(w, r)
}
func bug21955WorkaroundInterceptor(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
if rq, ok := req.(*repository.RepoQuery); ok {
repo, err := url.QueryUnescape(rq.Repo)
if err != nil {
return nil, err
}
rq.Repo = repo
} else if rk, ok := req.(*repository.RepoKsonnetQuery); ok {
repo, err := url.QueryUnescape(rk.Repo)
if err != nil {
return nil, err
}
rk.Repo = repo
} else if ru, ok := req.(*repository.RepoUpdateRequest); ok {
repo, err := url.QueryUnescape(ru.Repo.Repo)
if err != nil {
return nil, err
}
ru.Repo.Repo = repo
} else if cq, ok := req.(*cluster.ClusterQuery); ok {
server, err := url.QueryUnescape(cq.Server)
if err != nil {
return nil, err
}
cq.Server = server
} else if cu, ok := req.(*cluster.ClusterUpdateRequest); ok {
server, err := url.QueryUnescape(cu.Cluster.Server)
if err != nil {
return nil, err
}
cu.Cluster.Server = server
}
return handler(ctx, req)
}