Skip to content
Snippets Groups Projects
Select Git revision
  • bf378a46377cd98bb00e7b4053c179474316ce27
  • main default protected
  • next
  • revert-31645-feat/rename-gradle-wrapper-validation-action
  • renovate/main-redis-5.x
  • fix/36615b-branch-reuse-no-cache
  • chore/punycode
  • fix/36615-branch-reuse-bug
  • refactor/pin-new-value
  • feat/36219--git-x509-signing
  • feat/structured-logger
  • hotfix/39.264.1
  • feat/skip-dangling
  • gh-readonly-queue/next/pr-36034-7a061c4ca1024a19e2c295d773d9642625d1c2be
  • hotfix/39.238.3
  • refactor/gitlab-auto-approve
  • feat/template-strings
  • gh-readonly-queue/next/pr-35654-137d934242c784e0c45d4b957362214f0eade1d7
  • fix/32307-global-extends-merging
  • fix/32307-global-extends-repositories
  • gh-readonly-queue/next/pr-35009-046ebf7cb84ab859f7fefceb5fa53a54ce9736f8
  • 41.34.1
  • 41.34.0
  • 41.33.0
  • 41.32.3
  • 41.32.2
  • 41.32.1
  • 41.32.0
  • 41.31.1
  • 41.31.0
  • 41.30.5
  • 41.30.4
  • 41.30.3
  • 41.30.2
  • 41.30.1
  • 41.30.0
  • 41.29.1
  • 41.29.0
  • 41.28.2
  • 41.28.1
  • 41.28.0
41 results

extract-update.spec.ts

Blame
  • create_source_git.go 9.80 KiB
    package main
    
    import (
    	"context"
    	"crypto/elliptic"
    	"fmt"
    	"io/ioutil"
    	"net/url"
    	"os"
    	"time"
    
    	sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
    	"github.com/manifoldco/promptui"
    	"github.com/spf13/cobra"
    	corev1 "k8s.io/api/core/v1"
    	"k8s.io/apimachinery/pkg/api/errors"
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	"k8s.io/apimachinery/pkg/types"
    	"k8s.io/apimachinery/pkg/util/wait"
    	"sigs.k8s.io/controller-runtime/pkg/client"
    
    	"github.com/fluxcd/toolkit/pkg/ssh"
    )
    
    var createSourceGitCmd = &cobra.Command{
    	Use:   "git [name]",
    	Short: "Create or update a git source",
    	Long: `
    The create source command generates a GitRepository resource and waits for it to sync.
    For Git over SSH, host and SSH keys are automatically generated and stored in a Kubernetes secret.
    For private Git repositories, the basic authentication credentials are stored in a Kubernetes secret.`,
    	Example: `  # Create a source from a public Git repository master branch
      create source git podinfo \
        --url=https://github.com/stefanprodan/podinfo \
        --branch=master
    
      # Create a source from a Git repository pinned to specific git tag
      create source git podinfo \
        --url=https://github.com/stefanprodan/podinfo \
        --tag="3.2.3"
    
      # Create a source from a public Git repository tag that matches a semver range
      create source git podinfo \
        --url=https://github.com/stefanprodan/podinfo \
        --tag-semver=">=3.2.0 <3.3.0"
    
      # Create a source from a Git repository using SSH authentication
      create source git podinfo \
        --url=ssh://git@github.com/stefanprodan/podinfo \
        --branch=master
    
      # Create a source from a Git repository using SSH authentication and an
      # ECDSA P-521 curve public key
      create source git podinfo \
        --url=ssh://git@github.com/stefanprodan/podinfo \
        --branch=master \
        --ssh-key-algorithm=ecdsa \
        --ssh-ecdsa-curve=p521
    
      # Create a source from a Git repository using basic authentication
      create source git podinfo \
        --url=https://github.com/stefanprodan/podinfo \
        --username=username \
        --password=password
    `,
    	RunE: createSourceGitCmdRun,
    }
    
    var (
    	sourceGitURL          string
    	sourceGitBranch       string
    	sourceGitTag          string
    	sourceGitSemver       string
    	sourceGitUsername     string
    	sourceGitPassword     string
    	sourceGitKeyAlgorithm PublicKeyAlgorithm = "rsa"
    	sourceGitRSABits      RSAKeyBits         = 2048
    	sourceGitECDSACurve                      = ECDSACurve{elliptic.P384()}
    )
    
    func init() {
    	createSourceGitCmd.Flags().StringVar(&sourceGitURL, "url", "", "git address, e.g. ssh://git@host/org/repository")
    	createSourceGitCmd.Flags().StringVar(&sourceGitBranch, "branch", "master", "git branch")
    	createSourceGitCmd.Flags().StringVar(&sourceGitTag, "tag", "", "git tag")
    	createSourceGitCmd.Flags().StringVar(&sourceGitSemver, "tag-semver", "", "git tag semver range")
    	createSourceGitCmd.Flags().StringVarP(&sourceGitUsername, "username", "u", "", "basic authentication username")
    	createSourceGitCmd.Flags().StringVarP(&sourceGitPassword, "password", "p", "", "basic authentication password")
    	createSourceGitCmd.Flags().Var(&sourceGitKeyAlgorithm, "ssh-key-algorithm", sourceGitKeyAlgorithm.Description())
    	createSourceGitCmd.Flags().Var(&sourceGitRSABits, "ssh-rsa-bits", sourceGitRSABits.Description())
    	createSourceGitCmd.Flags().Var(&sourceGitECDSACurve, "ssh-ecdsa-curve", sourceGitECDSACurve.Description())
    
    	createSourceCmd.AddCommand(createSourceGitCmd)
    }
    
    func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
    	if len(args) < 1 {
    		return fmt.Errorf("source name is required")
    	}
    	name := args[0]
    
    	if sourceGitURL == "" {
    		return fmt.Errorf("git-url is required")
    	}
    
    	tmpDir, err := ioutil.TempDir("", name)
    	if err != nil {
    		return err
    	}
    	defer os.RemoveAll(tmpDir)
    
    	u, err := url.Parse(sourceGitURL)
    	if err != nil {
    		return fmt.Errorf("git URL parse failed: %w", err)
    	}
    
    	ctx, cancel := context.WithTimeout(context.Background(), timeout)
    	defer cancel()
    
    	kubeClient, err := utils.kubeClient(kubeconfig)
    	if err != nil {
    		return err
    	}
    
    	withAuth := false
    	// TODO(hidde): move all auth prep to separate func?
    	if u.Scheme == "ssh" {
    		logAction("generating deploy key pair")
    		pair, err := generateKeyPair(ctx)
    		if err != nil {
    			return err
    		}
    
    		fmt.Printf("%s", pair.PublicKey)
    		prompt := promptui.Prompt{
    			Label:     "Have you added the deploy key to your repository",
    			IsConfirm: true,
    		}
    		if _, err := prompt.Run(); err != nil {
    			return fmt.Errorf("aborting")
    		}
    
    		logAction("collecting preferred public key from SSH server")
    		hostKey, err := scanHostKey(ctx, u)
    		if err != nil {
    			return err
    		}
    		logSuccess("collected public key from SSH server:")
    		fmt.Printf("%s", hostKey)
    
    		logAction("applying secret with keys")
    		secret := corev1.Secret{
    			ObjectMeta: metav1.ObjectMeta{
    				Name:      name,
    				Namespace: namespace,
    			},
    			StringData: map[string]string{
    				"identity":     string(pair.PrivateKey),
    				"identity.pub": string(pair.PublicKey),
    				"known_hosts":  string(hostKey),
    			},
    		}
    		if err := upsertSecret(ctx, kubeClient, secret); err != nil {
    			return err
    		}
    		withAuth = true
    	} else if sourceGitUsername != "" && sourceGitPassword != "" {
    		logAction("applying secret with basic auth credentials")
    		secret := corev1.Secret{
    			ObjectMeta: metav1.ObjectMeta{
    				Name:      name,
    				Namespace: namespace,
    			},
    			StringData: map[string]string{
    				"username": sourceGitUsername,
    				"password": sourceGitPassword,
    			},
    		}
    		if err := upsertSecret(ctx, kubeClient, secret); err != nil {
    			return err
    		}
    		withAuth = true
    	}
    
    	if withAuth {
    		logSuccess("authentication configured")
    	}
    
    	logGenerate("generating source")
    
    	gitRepository := sourcev1.GitRepository{
    		ObjectMeta: metav1.ObjectMeta{
    			Name:      name,
    			Namespace: namespace,
    		},
    		Spec: sourcev1.GitRepositorySpec{
    			URL: sourceGitURL,
    			Interval: metav1.Duration{
    				Duration: interval,
    			},
    			Reference: &sourcev1.GitRepositoryRef{},
    		},
    	}
    
    	if withAuth {
    		gitRepository.Spec.SecretRef = &corev1.LocalObjectReference{
    			Name: name,
    		}
    	}
    
    	if sourceGitSemver != "" {
    		gitRepository.Spec.Reference.SemVer = sourceGitSemver
    	} else if sourceGitTag != "" {
    		gitRepository.Spec.Reference.Tag = sourceGitTag
    	} else {
    		gitRepository.Spec.Reference.Branch = sourceGitBranch
    	}
    
    	logAction("applying source")
    	if err := upsertGitRepository(ctx, kubeClient, gitRepository); err != nil {
    		return err
    	}
    
    	logWaiting("waiting for git sync")
    	if err := wait.PollImmediate(pollInterval, timeout,
    		isGitRepositoryReady(ctx, kubeClient, name, namespace)); err != nil {
    		return err
    	}
    
    	logSuccess("git sync completed")
    
    	namespacedName := types.NamespacedName{
    		Namespace: namespace,
    		Name:      name,
    	}
    	err = kubeClient.Get(ctx, namespacedName, &gitRepository)
    	if err != nil {
    		return fmt.Errorf("git sync failed: %w", err)
    	}
    
    	if gitRepository.Status.Artifact != nil {
    		logSuccess("fetched revision %s", gitRepository.Status.Artifact.Revision)
    	} else {
    		return fmt.Errorf("git sync failed, artifact not found")
    	}
    
    	return nil
    }
    
    func generateKeyPair(ctx context.Context) (*ssh.KeyPair, error) {
    	var keyGen ssh.KeyPairGenerator
    	switch sourceGitKeyAlgorithm.String() {
    	case "rsa":
    		keyGen = ssh.NewRSAGenerator(int(sourceGitRSABits))
    	case "ecdsa":
    		keyGen = ssh.NewECDSAGenerator(sourceGitECDSACurve.Curve)
    	case "ed25519":
    		keyGen = ssh.NewEd25519Generator()
    	default:
    		return nil, fmt.Errorf("unsupported public key algorithm '%s'", sourceGitKeyAlgorithm.String())
    	}
    	pair, err := keyGen.Generate()
    	if err != nil {
    		return nil, fmt.Errorf("key pair generation failed: %w", err)
    	}
    	return pair, nil
    }
    
    func scanHostKey(ctx context.Context, url *url.URL) ([]byte, error) {
    	host := url.Host
    	if url.Port() == "" {
    		host = host + ":22"
    	}
    	hostKey, err := ssh.ScanHostKey(host, 30*time.Second)
    	if err != nil {
    		return nil, fmt.Errorf("SSH key scan for host '%s' failed: %w", host, err)
    	}
    	return hostKey, nil
    }
    
    func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.Secret) error {
    	namespacedName := types.NamespacedName{
    		Namespace: secret.GetNamespace(),
    		Name:      secret.GetName(),
    	}
    
    	var existing corev1.Secret
    	err := kubeClient.Get(ctx, namespacedName, &existing)
    	if err != nil {
    		if errors.IsNotFound(err) {
    			if err := kubeClient.Create(ctx, &secret); err != nil {
    				return err
    			} else {
    				return nil
    			}
    		}
    		return err
    	}
    
    	existing.StringData = secret.StringData
    	if err := kubeClient.Update(ctx, &existing); err != nil {
    		return err
    	}
    	return nil
    }
    
    func upsertGitRepository(ctx context.Context, kubeClient client.Client, gitRepository sourcev1.GitRepository) error {
    	namespacedName := types.NamespacedName{
    		Namespace: gitRepository.GetNamespace(),
    		Name:      gitRepository.GetName(),
    	}
    
    	var existing sourcev1.GitRepository
    	err := kubeClient.Get(ctx, namespacedName, &existing)
    	if err != nil {
    		if errors.IsNotFound(err) {
    			if err := kubeClient.Create(ctx, &gitRepository); err != nil {
    				return err
    			} else {
    				logSuccess("source created")
    				return nil
    			}
    		}
    		return err
    	}
    
    	existing.Spec = gitRepository.Spec
    	if err := kubeClient.Update(ctx, &existing); err != nil {
    		return err
    	}
    
    	logSuccess("source updated")
    	return nil
    }
    
    func isGitRepositoryReady(ctx context.Context, kubeClient client.Client, name, namespace string) wait.ConditionFunc {
    	return func() (bool, error) {
    		var gitRepository sourcev1.GitRepository
    		namespacedName := types.NamespacedName{
    			Namespace: namespace,
    			Name:      name,
    		}
    
    		err := kubeClient.Get(ctx, namespacedName, &gitRepository)
    		if err != nil {
    			return false, err
    		}
    
    		for _, condition := range gitRepository.Status.Conditions {
    			if condition.Type == sourcev1.ReadyCondition {
    				if condition.Status == corev1.ConditionTrue {
    					return true, nil
    				} else if condition.Status == corev1.ConditionFalse {
    					return false, fmt.Errorf(condition.Message)
    				}
    			}
    		}
    		return false, nil
    	}
    }