Support persistence of repos as secrets

This commit is contained in:
Jesse Suen 2018-02-21 03:01:43 -08:00
parent f9bc9bd835
commit d7d7821c3e
No known key found for this signature in database
GPG key ID: 90C911E8A6106562
11 changed files with 176 additions and 33 deletions

2
Gopkg.lock generated
View file

@ -583,6 +583,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "0aa93ce59362fb4c878e843ba8ebed06d419f8aa29012f099a213aded7b4940c"
inputs-digest = "477de633045a1e9822c4e605e845790162eb15d9a0d414156b790b2b359ab48b"
solver-name = "gps-cdcl"
solver-version = 1

View file

@ -4,7 +4,7 @@ import (
"github.com/argoproj/argo-cd/errors"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
"github.com/argoproj/argo-cd/server"
"github.com/argoproj/argo-cd/util/cmd"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/kube"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -40,6 +40,6 @@ func NewCommand() *cobra.Command {
command.Flags().StringVar(&kubeConfig, "kubeconfig", "", "Kubernetes config (used when running outside of cluster)")
command.Flags().StringVar(&configMap, "configmap", defaultArgoCDConfigMap, "Name of K8s configmap to retrieve argocd configuration")
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
command.AddCommand(cmd.NewVersionCmd(cliName))
command.AddCommand(cli.NewVersionCmd(cliName))
return command
}

View file

@ -1,17 +1,21 @@
package commands
import (
"bufio"
"context"
"fmt"
"os"
"syscall"
"text/tabwriter"
"github.com/argoproj/argo-cd/errors"
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/server/repository"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/git"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
"google.golang.org/grpc"
)
@ -34,6 +38,9 @@ func NewRepoCommand() *cobra.Command {
// NewRepoAddCommand returns a new instance of an `argocd repo add` command
func NewRepoAddCommand() *cobra.Command {
var (
repo appsv1.Respository
)
var command = &cobra.Command{
Use: "add",
Short: fmt.Sprintf("%s repo add REPO", cliName),
@ -42,19 +49,44 @@ func NewRepoAddCommand() *cobra.Command {
c.HelpFunc()(c, args)
os.Exit(1)
}
repo.Repo = args[0]
err := git.TestRepo(repo.Repo, repo.Username, repo.Password)
if err != nil {
if repo.Username != "" && repo.Password != "" {
// if everything was supplied, one of the inputs was definitely bad
log.Fatal(err)
}
// If we can't test the repo, it's probably private. Prompt for credentials and try again.
promptCredentials(&repo)
err = git.TestRepo(repo.Repo, repo.Username, repo.Password)
}
errors.CheckError(err)
conn, repoIf := NewRepoClient()
defer util.Close(conn)
repo := &appsv1.Respository{
Repo: args[0],
}
repo, err := repoIf.Create(context.Background(), repo)
createdRepo, err := repoIf.Create(context.Background(), &repo)
errors.CheckError(err)
fmt.Printf("repository '%s' added\n", repo.Repo)
fmt.Printf("repository '%s' added\n", createdRepo.Repo)
},
}
command.Flags().StringVar(&repo.Username, "username", "", "username to the repository")
command.Flags().StringVar(&repo.Password, "password", "", "password to the repository")
return command
}
func promptCredentials(repo *appsv1.Respository) {
reader := bufio.NewReader(os.Stdin)
if repo.Username == "" {
fmt.Print("Username: ")
username, _ := reader.ReadString('\n')
repo.Username = username
}
if repo.Password == "" {
fmt.Print("Password: ")
bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin))
repo.Password = string(bytePassword)
}
}
// NewRepoRemoveCommand returns a new instance of an `argocd repo list` command
func NewRepoRemoveCommand() *cobra.Command {
var command = &cobra.Command{
@ -87,10 +119,11 @@ func NewRepoListCommand() *cobra.Command {
repos, err := repoIf.List(context.Background(), &repository.RepoQuery{})
errors.CheckError(err)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "REPO\n")
fmt.Fprintf(w, "REPO\tUSER\n")
for _, r := range repos.Items {
fmt.Fprintf(w, "%s\n", r.Repo)
fmt.Fprintf(w, "%s\t%s\n", r.Repo, r.Username)
}
w.Flush()
},
}
return command

View file

@ -1,7 +1,7 @@
package commands
import (
"github.com/argoproj/argo-cd/util/cmd"
"github.com/argoproj/argo-cd/util/cli"
"github.com/spf13/cobra"
"k8s.io/client-go/tools/clientcmd"
// load the gcp plugin (required to authenticate against GKE clusters).
@ -34,7 +34,7 @@ func NewCommand() *cobra.Command {
clientcmd.BindOverrideFlags(&globalArgs.kubeConfigOverrides, command.PersistentFlags(), clientcmd.RecommendedConfigOverrideFlags(""))
command.PersistentFlags().StringVar(&globalArgs.logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
command.AddCommand(cmd.NewVersionCmd(cliName))
command.AddCommand(cli.NewVersionCmd(cliName))
command.AddCommand(NewClusterCommand())
command.AddCommand(NewApplicationCommand())
command.AddCommand(NewRepoCommand())

View file

@ -3,9 +3,12 @@ package common
const (
// MetadataPrefix is the prefix used for our labels and annotations
MetadataPrefix = "argocd.argoproj.io"
// SecretTypeRepository indicates the data type which argocd stores as a k8s secret
SecretTypeRepository = "repository"
)
var (
// LabelKeyRepo contains the repository URL
LabelKeyRepo = MetadataPrefix + "/repo"
// LabelKeySecretType contains the type of argocd secret (currently this is just 'repo')
LabelKeySecretType = MetadataPrefix + "/secret-type"
)

View file

@ -4,7 +4,7 @@ import (
log "github.com/sirupsen/logrus"
)
// CheckError is a convenience function to exit if there was error is non-nil
// CheckError is a convenience function to exit if an error is non-nil and exit if it was
func CheckError(err error) {
if err != nil {
log.Fatal(err)

View file

@ -1,12 +1,19 @@
package repository
import (
"fmt"
"hash/fnv"
"strings"
"github.com/argoproj/argo-cd/common"
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
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"
apiv1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
@ -33,7 +40,10 @@ func NewServer(namespace string, kubeclientset kubernetes.Interface, appclientse
func (s *Server) List(ctx context.Context, q *RepoQuery) (*appsv1.RespositoryList, error) {
listOpts := metav1.ListOptions{}
labelSelector := labels.NewSelector()
req, _ := labels.NewRequirement(common.LabelKeyRepo, selection.Exists, nil)
req, err := labels.NewRequirement(common.LabelKeySecretType, selection.Equals, []string{common.SecretTypeRepository})
if err != nil {
return nil, err
}
labelSelector = labelSelector.Add(*req)
listOpts.LabelSelector = labelSelector.String()
repoSecrets, err := s.kubeclientset.CoreV1().Secrets(s.ns).List(listOpts)
@ -52,20 +62,28 @@ func (s *Server) List(ctx context.Context, q *RepoQuery) (*appsv1.RespositoryLis
// Create creates a repository
func (s *Server) Create(ctx context.Context, r *appsv1.Respository) (*appsv1.Respository, error) {
repoURL := git.NormalizeGitURL(r.Repo)
cmName := repoURLToSecretName(repoURL)
err := git.TestRepo(repoURL, r.Username, r.Password)
if err != nil {
return nil, err
}
secName := repoURLToSecretName(repoURL)
repoSecret := &apiv1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: cmName,
Name: secName,
Labels: map[string]string{
common.LabelKeyRepo: repoURL,
common.LabelKeySecretType: common.SecretTypeRepository,
},
},
}
repoSecret.StringData = make(map[string]string)
repoSecret.StringData["repository"] = repoURL
repoSecret.StringData["username"] = r.Username
repoSecret.StringData["password"] = r.Password
repoSecret, err := s.kubeclientset.CoreV1().Secrets(s.ns).Create(repoSecret)
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", repoURL)
}
return nil, err
}
return secretToRepo(repoSecret), nil
@ -73,8 +91,8 @@ func (s *Server) Create(ctx context.Context, r *appsv1.Respository) (*appsv1.Res
// Get returns a repository by URL
func (s *Server) Get(ctx context.Context, q *RepoQuery) (*appsv1.Respository, error) {
cmName := repoURLToSecretName(q.Repo)
repoSecret, err := s.kubeclientset.CoreV1().Secrets(s.ns).Get(cmName, metav1.GetOptions{})
secName := repoURLToSecretName(q.Repo)
repoSecret, err := s.kubeclientset.CoreV1().Secrets(s.ns).Get(secName, metav1.GetOptions{})
if err != nil {
return nil, err
}
@ -83,14 +101,21 @@ func (s *Server) Get(ctx context.Context, q *RepoQuery) (*appsv1.Respository, er
// Update updates a repository
func (s *Server) Update(ctx context.Context, r *appsv1.Respository) (*appsv1.Respository, error) {
cmName := repoURLToSecretName(r.Repo)
repoSecret, err := s.kubeclientset.CoreV1().Secrets(s.ns).Get(cmName, metav1.GetOptions{})
secName := repoURLToSecretName(r.Repo)
repoSecret, err := s.kubeclientset.CoreV1().Secrets(s.ns).Get(secName, metav1.GetOptions{})
if err != nil {
return nil, err
}
repoSecret.StringData = make(map[string]string)
repoSecret.StringData["repository"] = r.Repo
repoSecret.StringData["username"] = r.Username
repoSecret.StringData["password"] = r.Password
err = git.TestRepo(r.Repo, r.Username, r.Password)
if err != nil {
return nil, err
}
repoSecret, err = s.kubeclientset.CoreV1().Secrets(s.ns).Update(repoSecret)
if err != nil {
return nil, err
@ -105,22 +130,26 @@ func (s *Server) UpdateREST(ctx context.Context, r *RepoUpdateRequest) (*appsv1.
// Delete updates a repository
func (s *Server) Delete(ctx context.Context, q *RepoQuery) (*RepoResponse, error) {
cmName := repoURLToSecretName(q.Repo)
err := s.kubeclientset.CoreV1().Secrets(s.ns).Delete(cmName, &metav1.DeleteOptions{})
secName := repoURLToSecretName(q.Repo)
err := s.kubeclientset.CoreV1().Secrets(s.ns).Delete(secName, &metav1.DeleteOptions{})
return &RepoResponse{}, err
}
// repoURLToSecretName converts a repo
// repoURLToSecretName hashes repo URL to the secret name using formula
// part of the original repo name is incorporated for debugging purposes
func repoURLToSecretName(repo string) string {
repoURL := git.NormalizeGitURL(repo)
return repoURL
repo = git.NormalizeGitURL(repo)
h := fnv.New32a()
_, _ = h.Write([]byte(repo))
parts := strings.Split(strings.TrimSuffix(repo, ".git"), "/")
return fmt.Sprintf("repo-%s-%v", parts[len(parts)-1], h.Sum32())
}
// secretToRepo converts a secret into a repository object
func secretToRepo(s *apiv1.Secret) *appsv1.Respository {
repo := appsv1.Respository{
Repo: s.ObjectMeta.Labels[common.LabelKeyRepo],
Repo: string(s.Data["repository"]),
Username: string(s.Data["username"]),
Password: string(s.Data["password"]),
}

View file

@ -12,6 +12,7 @@ import (
"github.com/argoproj/argo-cd/server/cluster"
"github.com/argoproj/argo-cd/server/repository"
"github.com/argoproj/argo-cd/server/version"
grpc_util "github.com/argoproj/argo-cd/util/grpc"
jsonutil "github.com/argoproj/argo-cd/util/json"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
@ -71,9 +72,11 @@ func (a *ArgoCDServer) Run() {
grpcS := grpc.NewServer(
grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
grpc_logrus.StreamServerInterceptor(a.log),
grpc_util.PanicLoggerStreamServerInterceptor(a.log),
)),
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
grpc_logrus.UnaryServerInterceptor(a.log),
grpc_util.PanicLoggerUnaryServerInterceptor(a.log),
)),
)
version.RegisterVersionServiceServer(grpcS, &version.Server{})

View file

@ -1,6 +1,6 @@
// Package cmd provides functionally common to various argo CLIs
package cmd
package cli
import (
"fmt"

View file

@ -1,7 +1,46 @@
package git
import (
"fmt"
"net/url"
"os"
"os/exec"
"regexp"
"strings"
)
// NormalizeGitURL normalizes a git URL for lookup and storage
func NormalizeGitURL(repoURL string) string {
func NormalizeGitURL(repo string) string {
// TODO: implement this
return repoURL
repo = strings.TrimSpace(repo)
return repo
}
// TestRepo tests if a repo exists and is accessible with the given credentials
func TestRepo(repo, username, password string) error {
repoURL, err := url.ParseRequestURI(repo)
if err != nil {
return err
}
repoURL.User = url.UserPassword(username, password)
cmd := exec.Command("git", "ls-remote", repoURL.String(), "HEAD")
env := os.Environ()
env = append(env, "GIT_ASKPASS=")
cmd.Env = env
_, err = cmd.Output()
if err != nil {
exErr := err.(*exec.ExitError)
errOutput := strings.Split(string(exErr.Stderr), "\n")[0]
errOutput = redactPassword(errOutput, password)
return fmt.Errorf("failed to test %s: %s", repo, errOutput)
}
return nil
}
func redactPassword(msg string, password string) string {
if password != "" {
passwordRegexp := regexp.MustCompile("\\b" + regexp.QuoteMeta(password) + "\\b")
msg = passwordRegexp.ReplaceAllString(msg, "*****")
}
return msg
}

36
util/grpc/grpc.go Normal file
View file

@ -0,0 +1,36 @@
package grpc
import (
"runtime/debug"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
// PanicLoggerUnaryServerInterceptor returns a new unary server interceptor for recovering from panics and returning error
func PanicLoggerUnaryServerInterceptor(log *logrus.Entry) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) {
defer func() {
if r := recover(); r != nil {
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
err = grpc.Errorf(codes.Internal, "%s", r)
}
}()
return handler(ctx, req)
}
}
// PanicLoggerStreamServerInterceptor returns a new streaming server interceptor for recovering from panics and returning error
func PanicLoggerStreamServerInterceptor(log *logrus.Entry) grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) (err error) {
defer func() {
if r := recover(); r != nil {
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
err = grpc.Errorf(codes.Internal, "%s", r)
}
}()
return handler(srv, stream)
}
}