github.com/argoproj/argo-cd@v1.8.7/util/db/repository.go (about)

     1  package db
     2  
     3  import (
     4  	"fmt"
     5  	"hash/fnv"
     6  	"strings"
     7  
     8  	"golang.org/x/net/context"
     9  	"google.golang.org/grpc/codes"
    10  	"google.golang.org/grpc/status"
    11  	apiv1 "k8s.io/api/core/v1"
    12  	apierr "k8s.io/apimachinery/pkg/api/errors"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  
    15  	log "github.com/sirupsen/logrus"
    16  
    17  	"github.com/argoproj/argo-cd/common"
    18  	appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
    19  	"github.com/argoproj/argo-cd/util/git"
    20  	"github.com/argoproj/argo-cd/util/settings"
    21  )
    22  
    23  const (
    24  	// Prefix to use for naming repository secrets
    25  	repoSecretPrefix = "repo"
    26  	// Prefix to use for naming credential template secrets
    27  	credSecretPrefix = "creds"
    28  	// The name of the key storing the username in the secret
    29  	username = "username"
    30  	// The name of the key storing the password in the secret
    31  	password = "password"
    32  	// The name of the key storing the SSH private in the secret
    33  	sshPrivateKey = "sshPrivateKey"
    34  	// The name of the key storing the TLS client cert data in the secret
    35  	tlsClientCertData = "tlsClientCertData"
    36  	// The name of the key storing the TLS client cert key in the secret
    37  	tlsClientCertKey = "tlsClientCertKey"
    38  )
    39  
    40  func (db *db) CreateRepository(ctx context.Context, r *appsv1.Repository) (*appsv1.Repository, error) {
    41  	repos, err := db.settingsMgr.GetRepositories()
    42  	if err != nil {
    43  		return nil, err
    44  	}
    45  
    46  	index := getRepositoryIndex(repos, r.Repo)
    47  	if index > -1 {
    48  		return nil, status.Errorf(codes.AlreadyExists, "repository '%s' already exists", r.Repo)
    49  	}
    50  
    51  	data := make(map[string][]byte)
    52  	if r.Username != "" {
    53  		data[username] = []byte(r.Username)
    54  	}
    55  	if r.Password != "" {
    56  		data[password] = []byte(r.Password)
    57  	}
    58  	if r.SSHPrivateKey != "" {
    59  		data[sshPrivateKey] = []byte(r.SSHPrivateKey)
    60  	}
    61  
    62  	repoInfo := settings.Repository{
    63  		URL:                   r.Repo,
    64  		Type:                  r.Type,
    65  		Name:                  r.Name,
    66  		InsecureIgnoreHostKey: r.IsInsecure(),
    67  		Insecure:              r.IsInsecure(),
    68  		EnableLFS:             r.EnableLFS,
    69  		EnableOci:             r.EnableOCI,
    70  	}
    71  	err = db.updateRepositorySecrets(&repoInfo, r)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	repos = append(repos, repoInfo)
    77  	err = db.settingsMgr.SaveRepositories(repos)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	return r, nil
    82  }
    83  
    84  // GetRepository returns a repository by URL. If the repository doesn't have any
    85  // credentials attached to it, checks if a credential set for the repo's URL is
    86  // configured and copies them to the returned repository data.
    87  func (db *db) GetRepository(ctx context.Context, repoURL string) (*appsv1.Repository, error) {
    88  	repos, err := db.settingsMgr.GetRepositories()
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	repo := &appsv1.Repository{Repo: repoURL}
    94  	index := getRepositoryIndex(repos, repoURL)
    95  	if index >= 0 {
    96  		repo, err = db.credentialsToRepository(repos[index])
    97  		if err != nil {
    98  			return nil, err
    99  		}
   100  	}
   101  
   102  	// Check for and copy repository credentials, if repo has none configured.
   103  	if !repo.HasCredentials() {
   104  		creds, err := db.GetRepositoryCredentials(ctx, repoURL)
   105  		if err == nil {
   106  			if creds != nil {
   107  				repo.CopyCredentialsFrom(creds)
   108  				repo.InheritedCreds = true
   109  			}
   110  		} else {
   111  			return nil, err
   112  		}
   113  	} else {
   114  		log.Debugf("%s has credentials", repo.Repo)
   115  	}
   116  
   117  	return repo, err
   118  }
   119  
   120  func (db *db) ListRepositories(ctx context.Context) ([]*appsv1.Repository, error) {
   121  	return db.listRepositories(ctx, nil)
   122  }
   123  
   124  func (db *db) listRepositories(ctx context.Context, repoType *string) ([]*appsv1.Repository, error) {
   125  	inRepos, err := db.settingsMgr.GetRepositories()
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	var repos []*appsv1.Repository
   131  	for _, inRepo := range inRepos {
   132  		if repoType == nil || *repoType == inRepo.Type {
   133  			r, err := db.GetRepository(ctx, inRepo.URL)
   134  			if err != nil {
   135  				return nil, err
   136  			}
   137  			repos = append(repos, r)
   138  		}
   139  	}
   140  	return repos, nil
   141  }
   142  
   143  func (db *db) credentialsToRepository(repoInfo settings.Repository) (*appsv1.Repository, error) {
   144  	repo := &appsv1.Repository{
   145  		Repo:                  repoInfo.URL,
   146  		Type:                  repoInfo.Type,
   147  		Name:                  repoInfo.Name,
   148  		InsecureIgnoreHostKey: repoInfo.InsecureIgnoreHostKey,
   149  		Insecure:              repoInfo.Insecure,
   150  		EnableLFS:             repoInfo.EnableLFS,
   151  		EnableOCI:             repoInfo.EnableOci,
   152  	}
   153  	err := db.unmarshalFromSecretsStr(map[*string]*apiv1.SecretKeySelector{
   154  		&repo.Username:          repoInfo.UsernameSecret,
   155  		&repo.Password:          repoInfo.PasswordSecret,
   156  		&repo.SSHPrivateKey:     repoInfo.SSHPrivateKeySecret,
   157  		&repo.TLSClientCertData: repoInfo.TLSClientCertDataSecret,
   158  		&repo.TLSClientCertKey:  repoInfo.TLSClientCertKeySecret,
   159  	}, make(map[string]*apiv1.Secret))
   160  	return repo, err
   161  }
   162  
   163  func (db *db) credentialsToRepositoryCredentials(repoInfo settings.RepositoryCredentials) (*appsv1.RepoCreds, error) {
   164  	creds := &appsv1.RepoCreds{
   165  		URL: repoInfo.URL,
   166  	}
   167  	err := db.unmarshalFromSecretsStr(map[*string]*apiv1.SecretKeySelector{
   168  		&creds.Username:          repoInfo.UsernameSecret,
   169  		&creds.Password:          repoInfo.PasswordSecret,
   170  		&creds.SSHPrivateKey:     repoInfo.SSHPrivateKeySecret,
   171  		&creds.TLSClientCertData: repoInfo.TLSClientCertDataSecret,
   172  		&creds.TLSClientCertKey:  repoInfo.TLSClientCertKeySecret,
   173  	}, make(map[string]*apiv1.Secret))
   174  	return creds, err
   175  }
   176  
   177  // UpdateRepository updates a repository
   178  func (db *db) UpdateRepository(ctx context.Context, r *appsv1.Repository) (*appsv1.Repository, error) {
   179  	repos, err := db.settingsMgr.GetRepositories()
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	index := getRepositoryIndex(repos, r.Repo)
   185  	if index < 0 {
   186  		return nil, status.Errorf(codes.NotFound, "repo '%s' not found", r.Repo)
   187  	}
   188  
   189  	repoInfo := repos[index]
   190  	err = db.updateRepositorySecrets(&repoInfo, r)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	// Update boolean settings
   196  	repoInfo.InsecureIgnoreHostKey = r.IsInsecure()
   197  	repoInfo.Insecure = r.IsInsecure()
   198  	repoInfo.EnableLFS = r.EnableLFS
   199  
   200  	repos[index] = repoInfo
   201  	err = db.settingsMgr.SaveRepositories(repos)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  	return r, nil
   206  }
   207  
   208  // Delete updates a repository
   209  func (db *db) DeleteRepository(ctx context.Context, repoURL string) error {
   210  	repos, err := db.settingsMgr.GetRepositories()
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	index := getRepositoryIndex(repos, repoURL)
   216  	if index < 0 {
   217  		return status.Errorf(codes.NotFound, "repo '%s' not found", repoURL)
   218  	}
   219  	err = db.updateRepositorySecrets(&repos[index], &appsv1.Repository{
   220  		SSHPrivateKey:     "",
   221  		Password:          "",
   222  		Username:          "",
   223  		TLSClientCertData: "",
   224  		TLSClientCertKey:  "",
   225  	})
   226  	if err != nil {
   227  		return err
   228  	}
   229  	repos = append(repos[:index], repos[index+1:]...)
   230  	return db.settingsMgr.SaveRepositories(repos)
   231  }
   232  
   233  // ListRepositoryCredentials returns a list of URLs that contain repo credential sets
   234  func (db *db) ListRepositoryCredentials(ctx context.Context) ([]string, error) {
   235  	repos, err := db.settingsMgr.GetRepositoryCredentials()
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  
   240  	urls := make([]string, len(repos))
   241  	for i := range repos {
   242  		urls[i] = repos[i].URL
   243  	}
   244  
   245  	return urls, nil
   246  }
   247  
   248  // GetRepositoryCredentials retrieves a repository credential set
   249  func (db *db) GetRepositoryCredentials(ctx context.Context, repoURL string) (*appsv1.RepoCreds, error) {
   250  	var credential *appsv1.RepoCreds
   251  
   252  	repoCredentials, err := db.settingsMgr.GetRepositoryCredentials()
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  	index := getRepositoryCredentialIndex(repoCredentials, repoURL)
   257  	if index >= 0 {
   258  		credential, err = db.credentialsToRepositoryCredentials(repoCredentials[index])
   259  		if err != nil {
   260  			return nil, err
   261  		}
   262  	}
   263  
   264  	return credential, err
   265  }
   266  
   267  // CreateRepositoryCredentials creates a repository credential set
   268  func (db *db) CreateRepositoryCredentials(ctx context.Context, r *appsv1.RepoCreds) (*appsv1.RepoCreds, error) {
   269  	creds, err := db.settingsMgr.GetRepositoryCredentials()
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  
   274  	index := getRepositoryCredentialIndex(creds, r.URL)
   275  	if index > -1 {
   276  		return nil, status.Errorf(codes.AlreadyExists, "repository credentials for '%s' already exists", r.URL)
   277  	}
   278  
   279  	repoInfo := settings.RepositoryCredentials{
   280  		URL: r.URL,
   281  	}
   282  
   283  	err = db.updateCredentialsSecret(&repoInfo, r)
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  
   288  	creds = append(creds, repoInfo)
   289  	err = db.settingsMgr.SaveRepositoryCredentials(creds)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  	return r, nil
   294  }
   295  
   296  // UpdateRepositoryCredentials updates a repository credential set
   297  func (db *db) UpdateRepositoryCredentials(ctx context.Context, r *appsv1.RepoCreds) (*appsv1.RepoCreds, error) {
   298  	repos, err := db.settingsMgr.GetRepositoryCredentials()
   299  	if err != nil {
   300  		return nil, err
   301  	}
   302  
   303  	index := getRepositoryCredentialIndex(repos, r.URL)
   304  	if index < 0 {
   305  		return nil, status.Errorf(codes.NotFound, "repository credentials '%s' not found", r.URL)
   306  	}
   307  
   308  	repoInfo := repos[index]
   309  	err = db.updateCredentialsSecret(&repoInfo, r)
   310  	if err != nil {
   311  		return nil, err
   312  	}
   313  
   314  	repos[index] = repoInfo
   315  	err = db.settingsMgr.SaveRepositoryCredentials(repos)
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  	return r, nil
   320  
   321  }
   322  
   323  // DeleteRepositoryCredentials deletes a repository credential set from config, and
   324  // also all the secrets which actually contained the credentials.
   325  func (db *db) DeleteRepositoryCredentials(ctx context.Context, name string) error {
   326  	repos, err := db.settingsMgr.GetRepositoryCredentials()
   327  	if err != nil {
   328  		return err
   329  	}
   330  
   331  	index := getRepositoryCredentialIndex(repos, name)
   332  	if index < 0 {
   333  		return status.Errorf(codes.NotFound, "repository credentials '%s' not found", name)
   334  	}
   335  	err = db.updateCredentialsSecret(&repos[index], &appsv1.RepoCreds{
   336  		SSHPrivateKey:     "",
   337  		Password:          "",
   338  		Username:          "",
   339  		TLSClientCertData: "",
   340  		TLSClientCertKey:  "",
   341  	})
   342  	if err != nil {
   343  		return err
   344  	}
   345  	repos = append(repos[:index], repos[index+1:]...)
   346  	return db.settingsMgr.SaveRepositoryCredentials(repos)
   347  }
   348  
   349  func (db *db) updateCredentialsSecret(credsInfo *settings.RepositoryCredentials, c *appsv1.RepoCreds) error {
   350  	r := &appsv1.Repository{
   351  		Repo:              c.URL,
   352  		Username:          c.Username,
   353  		Password:          c.Password,
   354  		SSHPrivateKey:     c.SSHPrivateKey,
   355  		TLSClientCertData: c.TLSClientCertData,
   356  		TLSClientCertKey:  c.TLSClientCertKey,
   357  	}
   358  	secretsData := make(map[string]map[string][]byte)
   359  
   360  	credsInfo.UsernameSecret = setSecretData(credSecretPrefix, r.Repo, secretsData, credsInfo.UsernameSecret, r.Username, username)
   361  	credsInfo.PasswordSecret = setSecretData(credSecretPrefix, r.Repo, secretsData, credsInfo.PasswordSecret, r.Password, password)
   362  	credsInfo.SSHPrivateKeySecret = setSecretData(credSecretPrefix, r.Repo, secretsData, credsInfo.SSHPrivateKeySecret, r.SSHPrivateKey, sshPrivateKey)
   363  	credsInfo.TLSClientCertDataSecret = setSecretData(credSecretPrefix, r.Repo, secretsData, credsInfo.TLSClientCertDataSecret, r.TLSClientCertData, tlsClientCertData)
   364  	credsInfo.TLSClientCertKeySecret = setSecretData(credSecretPrefix, r.Repo, secretsData, credsInfo.TLSClientCertKeySecret, r.TLSClientCertKey, tlsClientCertKey)
   365  	for k, v := range secretsData {
   366  		err := db.upsertSecret(k, v)
   367  		if err != nil {
   368  			return err
   369  		}
   370  	}
   371  	return nil
   372  }
   373  
   374  func (db *db) updateRepositorySecrets(repoInfo *settings.Repository, r *appsv1.Repository) error {
   375  	secretsData := make(map[string]map[string][]byte)
   376  
   377  	repoInfo.UsernameSecret = setSecretData(repoSecretPrefix, r.Repo, secretsData, repoInfo.UsernameSecret, r.Username, username)
   378  	repoInfo.PasswordSecret = setSecretData(repoSecretPrefix, r.Repo, secretsData, repoInfo.PasswordSecret, r.Password, password)
   379  	repoInfo.SSHPrivateKeySecret = setSecretData(repoSecretPrefix, r.Repo, secretsData, repoInfo.SSHPrivateKeySecret, r.SSHPrivateKey, sshPrivateKey)
   380  	repoInfo.TLSClientCertDataSecret = setSecretData(repoSecretPrefix, r.Repo, secretsData, repoInfo.TLSClientCertDataSecret, r.TLSClientCertData, tlsClientCertData)
   381  	repoInfo.TLSClientCertKeySecret = setSecretData(repoSecretPrefix, r.Repo, secretsData, repoInfo.TLSClientCertKeySecret, r.TLSClientCertKey, tlsClientCertKey)
   382  	for k, v := range secretsData {
   383  		err := db.upsertSecret(k, v)
   384  		if err != nil {
   385  			return err
   386  		}
   387  	}
   388  	return nil
   389  }
   390  
   391  // Set data to be stored in a given secret used for repository credentials and templates.
   392  // The name of the secret is a combination of the prefix given, and a calculated value
   393  // from the repository or template URL.
   394  func setSecretData(prefix string, url string, secretsData map[string]map[string][]byte, secretKey *apiv1.SecretKeySelector, value string, defaultKeyName string) *apiv1.SecretKeySelector {
   395  	if secretKey == nil && value != "" {
   396  		secretKey = &apiv1.SecretKeySelector{
   397  			LocalObjectReference: apiv1.LocalObjectReference{Name: repoURLToSecretName(prefix, url)},
   398  			Key:                  defaultKeyName,
   399  		}
   400  	}
   401  
   402  	if secretKey != nil {
   403  		data, ok := secretsData[secretKey.Name]
   404  		if !ok {
   405  			data = map[string][]byte{}
   406  		}
   407  		if value != "" {
   408  			data[secretKey.Key] = []byte(value)
   409  		}
   410  		secretsData[secretKey.Name] = data
   411  	}
   412  
   413  	if value == "" {
   414  		secretKey = nil
   415  	}
   416  
   417  	return secretKey
   418  }
   419  
   420  func (db *db) upsertSecret(name string, data map[string][]byte) error {
   421  	secret, err := db.kubeclientset.CoreV1().Secrets(db.ns).Get(context.Background(), name, metav1.GetOptions{})
   422  	if err != nil {
   423  		if apierr.IsNotFound(err) {
   424  			if len(data) == 0 {
   425  				return nil
   426  			}
   427  			_, err = db.kubeclientset.CoreV1().Secrets(db.ns).Create(context.Background(), &apiv1.Secret{
   428  				ObjectMeta: metav1.ObjectMeta{
   429  					Name: name,
   430  					Annotations: map[string]string{
   431  						common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD,
   432  					},
   433  				},
   434  				Data: data,
   435  			}, metav1.CreateOptions{})
   436  			if err != nil {
   437  				return err
   438  			}
   439  		}
   440  	} else {
   441  		for _, key := range []string{username, password, sshPrivateKey, tlsClientCertData, tlsClientCertKey} {
   442  			if secret.Data == nil {
   443  				secret.Data = make(map[string][]byte)
   444  			}
   445  			if val, ok := data[key]; ok && len(val) > 0 {
   446  				secret.Data[key] = val
   447  			} else {
   448  				delete(secret.Data, key)
   449  			}
   450  		}
   451  		if len(secret.Data) == 0 {
   452  			isManagedByArgo := (secret.Annotations != nil && secret.Annotations[common.AnnotationKeyManagedBy] == common.AnnotationValueManagedByArgoCD) ||
   453  				(secret.Labels != nil && secret.Labels[common.LabelKeySecretType] == "repository")
   454  			if isManagedByArgo {
   455  				return db.kubeclientset.CoreV1().Secrets(db.ns).Delete(context.Background(), name, metav1.DeleteOptions{})
   456  			}
   457  			return nil
   458  		} else {
   459  			_, err = db.kubeclientset.CoreV1().Secrets(db.ns).Update(context.Background(), secret, metav1.UpdateOptions{})
   460  			if err != nil {
   461  				return err
   462  			}
   463  		}
   464  	}
   465  	return nil
   466  }
   467  
   468  func getRepositoryIndex(repos []settings.Repository, repoURL string) int {
   469  	for i, repo := range repos {
   470  		if git.SameURL(repo.URL, repoURL) {
   471  			return i
   472  		}
   473  	}
   474  	return -1
   475  }
   476  
   477  // getRepositoryCredentialIndex returns the index of the best matching repository credential
   478  // configuration, i.e. the one with the longest match
   479  func getRepositoryCredentialIndex(repoCredentials []settings.RepositoryCredentials, repoURL string) int {
   480  	var max, idx int = 0, -1
   481  	repoURL = git.NormalizeGitURL(repoURL)
   482  	for i, cred := range repoCredentials {
   483  		credUrl := git.NormalizeGitURL(cred.URL)
   484  		if strings.HasPrefix(repoURL, credUrl) {
   485  			if len(credUrl) > max {
   486  				max = len(credUrl)
   487  				idx = i
   488  			}
   489  		}
   490  	}
   491  	return idx
   492  }
   493  
   494  // repoURLToSecretName hashes repo URL to a secret name using a formula. This is used when
   495  // repositories are _imperatively_ created and need its credentials to be stored in a secret.
   496  // NOTE: this formula should not be considered stable and may change in future releases.
   497  // Do NOT rely on this formula as a means of secret lookup, only secret creation.
   498  func repoURLToSecretName(prefix string, repo string) string {
   499  	h := fnv.New32a()
   500  	_, _ = h.Write([]byte(repo))
   501  	return fmt.Sprintf("%s-%v", prefix, h.Sum32())
   502  }