mirror of https://github.com/fluxcd/flux2.git
Merge pull request #968 from fluxcd/go-git-providers-bootstrap
commit
0d2f6bf02d
@ -0,0 +1,252 @@
|
||||
/*
|
||||
Copyright 2021 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/fluxcd/flux2/internal/bootstrap"
|
||||
"github.com/fluxcd/flux2/internal/bootstrap/git/gogit"
|
||||
"github.com/fluxcd/flux2/internal/flags"
|
||||
"github.com/fluxcd/flux2/internal/utils"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/install"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/sync"
|
||||
)
|
||||
|
||||
var bootstrapGitCmd = &cobra.Command{
|
||||
Use: "git",
|
||||
Short: "Bootstrap toolkit components in a Git repository",
|
||||
Long: `The bootstrap git command commits the toolkit components manifests to the
|
||||
branch of a Git repository. It then configures the target cluster to synchronize with
|
||||
the repository. If the toolkit components are present on the cluster, the bootstrap
|
||||
command will perform an upgrade if needed.`,
|
||||
Example: ` # Run bootstrap for a Git repository and authenticate with your SSH agent
|
||||
flux bootstrap git --url=ssh://git@example.com/repository.git
|
||||
|
||||
# Run bootstrap for a Git repository and authenticate using a password
|
||||
flux bootstrap git --url=https://example.com/repository.git --password=<password>
|
||||
|
||||
# Run bootstrap for a Git repository with a passwordless private key
|
||||
flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key>
|
||||
`,
|
||||
RunE: bootstrapGitCmdRun,
|
||||
}
|
||||
|
||||
type gitFlags struct {
|
||||
url string
|
||||
interval time.Duration
|
||||
path flags.SafeRelativePath
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
var gitArgs gitFlags
|
||||
|
||||
func init() {
|
||||
bootstrapGitCmd.Flags().StringVar(&gitArgs.url, "url", "", "Git repository URL")
|
||||
bootstrapGitCmd.Flags().DurationVar(&gitArgs.interval, "interval", time.Minute, "sync interval")
|
||||
bootstrapGitCmd.Flags().Var(&gitArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
|
||||
bootstrapGitCmd.Flags().StringVarP(&gitArgs.username, "username", "u", "git", "basic authentication username")
|
||||
bootstrapGitCmd.Flags().StringVarP(&gitArgs.password, "password", "p", "", "basic authentication password")
|
||||
|
||||
bootstrapCmd.AddCommand(bootstrapGitCmd)
|
||||
}
|
||||
|
||||
func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
|
||||
if err := bootstrapValidate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repositoryURL, err := url.Parse(gitArgs.url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gitAuth, err := transportForURL(repositoryURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
||||
defer cancel()
|
||||
|
||||
kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Manifest base
|
||||
if ver, err := getVersion(bootstrapArgs.version); err == nil {
|
||||
bootstrapArgs.version = ver
|
||||
}
|
||||
manifestsBase, err := buildEmbeddedManifestBase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(manifestsBase)
|
||||
|
||||
// Lazy go-git repository
|
||||
tmpDir, err := ioutil.TempDir("", "flux-bootstrap-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temporary working dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
gitClient := gogit.New(tmpDir, gitAuth)
|
||||
|
||||
// Install manifest config
|
||||
installOptions := install.Options{
|
||||
BaseURL: rootArgs.defaults.BaseURL,
|
||||
Version: bootstrapArgs.version,
|
||||
Namespace: rootArgs.namespace,
|
||||
Components: bootstrapComponents(),
|
||||
Registry: bootstrapArgs.registry,
|
||||
ImagePullSecret: bootstrapArgs.imagePullSecret,
|
||||
WatchAllNamespaces: bootstrapArgs.watchAllNamespaces,
|
||||
NetworkPolicy: bootstrapArgs.networkPolicy,
|
||||
LogLevel: bootstrapArgs.logLevel.String(),
|
||||
NotificationController: rootArgs.defaults.NotificationController,
|
||||
ManifestFile: rootArgs.defaults.ManifestFile,
|
||||
Timeout: rootArgs.timeout,
|
||||
TargetPath: gitArgs.path.String(),
|
||||
ClusterDomain: bootstrapArgs.clusterDomain,
|
||||
TolerationKeys: bootstrapArgs.tolerationKeys,
|
||||
}
|
||||
if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" {
|
||||
installOptions.BaseURL = customBaseURL
|
||||
}
|
||||
|
||||
// Source generation and secret config
|
||||
secretOpts := sourcesecret.Options{
|
||||
Name: bootstrapArgs.secretName,
|
||||
Namespace: rootArgs.namespace,
|
||||
TargetPath: gitArgs.path.String(),
|
||||
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
|
||||
}
|
||||
if bootstrapArgs.tokenAuth {
|
||||
secretOpts.Username = gitArgs.username
|
||||
secretOpts.Password = gitArgs.password
|
||||
|
||||
if bootstrapArgs.caFile != "" {
|
||||
secretOpts.CAFilePath = bootstrapArgs.caFile
|
||||
}
|
||||
|
||||
// Configure repository URL to match auth config for sync.
|
||||
repositoryURL.User = nil
|
||||
repositoryURL.Scheme = "https"
|
||||
repositoryURL.Host = repositoryURL.Hostname()
|
||||
} else {
|
||||
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
|
||||
secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
|
||||
secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
|
||||
|
||||
// Configure repository URL to match auth config for sync.
|
||||
repositoryURL.User = url.User(gitArgs.username)
|
||||
repositoryURL.Scheme = "ssh"
|
||||
repositoryURL.Host = repositoryURL.Hostname()
|
||||
if bootstrapArgs.sshHostname != "" {
|
||||
repositoryURL.Host = bootstrapArgs.sshHostname
|
||||
}
|
||||
|
||||
// Configure last as it depends on the config above.
|
||||
secretOpts.SSHHostname = repositoryURL.Host
|
||||
}
|
||||
|
||||
// Sync manifest config
|
||||
syncOpts := sync.Options{
|
||||
Interval: gitArgs.interval,
|
||||
Name: rootArgs.namespace,
|
||||
Namespace: rootArgs.namespace,
|
||||
URL: repositoryURL.String(),
|
||||
Branch: bootstrapArgs.branch,
|
||||
Secret: bootstrapArgs.secretName,
|
||||
TargetPath: gitArgs.path.String(),
|
||||
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
|
||||
GitImplementation: sourceGitArgs.gitImplementation.String(),
|
||||
}
|
||||
|
||||
// Bootstrap config
|
||||
bootstrapOpts := []bootstrap.GitOption{
|
||||
bootstrap.WithRepositoryURL(gitArgs.url),
|
||||
bootstrap.WithBranch(bootstrapArgs.branch),
|
||||
bootstrap.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
|
||||
bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix),
|
||||
bootstrap.WithKubeconfig(rootArgs.kubeconfig, rootArgs.kubecontext),
|
||||
bootstrap.WithPostGenerateSecretFunc(promptPublicKey),
|
||||
bootstrap.WithLogger(logger),
|
||||
}
|
||||
|
||||
// Setup bootstrapper with constructed configs
|
||||
b, err := bootstrap.NewPlainGitProvider(gitClient, kubeClient, bootstrapOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run
|
||||
return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout)
|
||||
}
|
||||
|
||||
// transportForURL constructs a transport.AuthMethod based on the scheme
|
||||
// of the given URL and the configured flags. If the protocol equals
|
||||
// "ssh" but no private key is configured, authentication using the local
|
||||
// SSH-agent is attempted.
|
||||
func transportForURL(u *url.URL) (transport.AuthMethod, error) {
|
||||
switch u.Scheme {
|
||||
case "https":
|
||||
return &http.BasicAuth{
|
||||
Username: gitArgs.username,
|
||||
Password: gitArgs.password,
|
||||
}, nil
|
||||
case "ssh":
|
||||
if bootstrapArgs.privateKeyFile != "" {
|
||||
return ssh.NewPublicKeysFromFile(u.User.Username(), bootstrapArgs.privateKeyFile, "")
|
||||
}
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("scheme %q is not supported", u.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
func promptPublicKey(ctx context.Context, secret corev1.Secret, _ sourcesecret.Options) error {
|
||||
ppk, ok := secret.StringData[sourcesecret.PublicKeySecretKey]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Successf("public key: %s", strings.TrimSpace(ppk))
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Please give the key access to your repository",
|
||||
IsConfirm: true,
|
||||
}
|
||||
_, err := prompt.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("aborting")
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
---
|
||||
title: "flux bootstrap git command"
|
||||
---
|
||||
## flux bootstrap git
|
||||
|
||||
Bootstrap toolkit components in a Git repository
|
||||
|
||||
### Synopsis
|
||||
|
||||
The bootstrap git command commits the toolkit components manifests to the
|
||||
branch of a Git repository. It then configures the target cluster to synchronize with
|
||||
the repository. If the toolkit components are present on the cluster, the bootstrap
|
||||
command will perform an upgrade if needed.
|
||||
|
||||
```
|
||||
flux bootstrap git [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
# Run bootstrap for a Git repository and authenticate with your SSH agent
|
||||
flux bootstrap git --url=ssh://git@example.com/repository.git
|
||||
|
||||
# Run bootstrap for a Git repository and authenticate using a password
|
||||
flux bootstrap git --url=https://example.com/repository.git --password=<password>
|
||||
|
||||
# Run bootstrap for a Git repository with a passwordless private key
|
||||
flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key>
|
||||
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for git
|
||||
--interval duration sync interval (default 1m0s)
|
||||
-p, --password string basic authentication password
|
||||
--path safeRelativePath path relative to the repository root, when specified the cluster sync will be scoped to this path
|
||||
--url string Git repository URL
|
||||
-u, --username string basic authentication username (default "git")
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
--author-email string author email for Git commits
|
||||
--author-name string author name for Git commits (default "Flux")
|
||||
--branch string default branch (for GitHub this must match the default branch setting for the organization) (default "main")
|
||||
--ca-file string path to TLS CA file used for validating self-signed certificates
|
||||
--cluster-domain string internal cluster domain (default "cluster.local")
|
||||
--commit-message-appendix string string to add to the commit messages, e.g. '[ci skip]'
|
||||
--components strings list of components, accepts comma-separated values (default [source-controller,kustomize-controller,helm-controller,notification-controller])
|
||||
--components-extra strings list of components in addition to those supplied or defaulted, accepts comma-separated values
|
||||
--context string kubernetes context to use
|
||||
--image-pull-secret string Kubernetes secret name used for pulling the toolkit images from a private registry
|
||||
--kubeconfig string absolute path to the kubeconfig file
|
||||
--log-level logLevel log level, available options are: (debug, info, error) (default info)
|
||||
-n, --namespace string the namespace scope for this operation (default "flux-system")
|
||||
--network-policy deny ingress access to the toolkit controllers from other namespaces using network policies (default true)
|
||||
--private-key-file string path to a private key file used for authenticating to the Git SSH server
|
||||
--registry string container registry where the toolkit images are published (default "ghcr.io/fluxcd")
|
||||
--secret-name string name of the secret the sync credentials can be found in or stored to (default "flux-system")
|
||||
--ssh-ecdsa-curve ecdsaCurve SSH ECDSA public key curve (p256, p384, p521) (default p384)
|
||||
--ssh-hostname string SSH hostname, to be used when the SSH host differs from the HTTPS one
|
||||
--ssh-key-algorithm publicKeyAlgorithm SSH public key algorithm (rsa, ecdsa, ed25519) (default rsa)
|
||||
--ssh-rsa-bits rsaKeyBits SSH RSA public key bit size (multiplies of 8) (default 2048)
|
||||
--timeout duration timeout for this operation (default 5m0s)
|
||||
--token-auth when enabled, the personal access token will be used instead of SSH deploy key
|
||||
--toleration-keys strings list of toleration keys used to schedule the components pods onto nodes with matching taints
|
||||
--verbose print generated objects
|
||||
-v, --version string toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases
|
||||
--watch-all-namespaces watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed (default true)
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [flux bootstrap](../flux_bootstrap/) - Bootstrap toolkit components
|
||||
|
@ -0,0 +1,198 @@
|
||||
/*
|
||||
Copyright 2021 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
apimeta "k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/install"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrReconciledWithWarning = errors.New("reconciled with warning")
|
||||
)
|
||||
|
||||
type Reconciler interface {
|
||||
// ReconcileComponents reconciles the components by generating the
|
||||
// manifests with the provided values, committing them to Git and
|
||||
// pushing to remote if there are any changes, and applying them
|
||||
// to the cluster.
|
||||
ReconcileComponents(ctx context.Context, manifestsBase string, options install.Options) error
|
||||
|
||||
// ReconcileSourceSecret reconciles the source secret by generating
|
||||
// a new secret with the provided values if the secret does not
|
||||
// already exists on the cluster, or if any of the configuration
|
||||
// options changed.
|
||||
ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error
|
||||
|
||||
// ReconcileSyncConfig reconciles the sync configuration by generating
|
||||
// the sync manifests with the provided values, committing them to Git
|
||||
// and pushing to remote if there are any changes.
|
||||
ReconcileSyncConfig(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error
|
||||
|
||||
// ConfirmHealthy confirms that the components and extra components in
|
||||
// install.Options are healthy.
|
||||
ConfirmHealthy(ctx context.Context, options install.Options, timeout time.Duration) error
|
||||
}
|
||||
|
||||
type RepositoryReconciler interface {
|
||||
// ReconcileRepository reconciles an external Git repository.
|
||||
ReconcileRepository(ctx context.Context) error
|
||||
}
|
||||
|
||||
type PostGenerateSecretFunc func(ctx context.Context, secret corev1.Secret, options sourcesecret.Options) error
|
||||
|
||||
func Run(ctx context.Context, reconciler Reconciler, manifestsBase string,
|
||||
installOpts install.Options, secretOpts sourcesecret.Options, syncOpts sync.Options,
|
||||
pollInterval, timeout time.Duration) error {
|
||||
|
||||
var err error
|
||||
if r, ok := reconciler.(RepositoryReconciler); ok {
|
||||
if err = r.ReconcileRepository(ctx); err != nil && !errors.Is(err, ErrReconciledWithWarning) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := reconciler.ReconcileComponents(ctx, manifestsBase, installOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := reconciler.ReconcileSourceSecret(ctx, secretOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := reconciler.ReconcileSyncConfig(ctx, syncOpts, pollInterval, timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := reconciler.ConfirmHealthy(ctx, installOpts, timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func mustInstallManifests(ctx context.Context, kube client.Client, namespace string) bool {
|
||||
namespacedName := types.NamespacedName{
|
||||
Namespace: namespace,
|
||||
Name: namespace,
|
||||
}
|
||||
var k kustomizev1.Kustomization
|
||||
if err := kube.Get(ctx, namespacedName, &k); err != nil {
|
||||
return true
|
||||
}
|
||||
return k.Status.LastAppliedRevision == ""
|
||||
}
|
||||
|
||||
func secretExists(ctx context.Context, kube client.Client, objKey client.ObjectKey) (bool, error) {
|
||||
if err := kube.Get(ctx, objKey, &corev1.Secret{}); err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func reconcileSecret(ctx context.Context, kube client.Client, secret corev1.Secret) error {
|
||||
objKey := client.ObjectKeyFromObject(&secret)
|
||||
var existing corev1.Secret
|
||||
err := kube.Get(ctx, objKey, &existing)
|
||||
if err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
return kube.Create(ctx, &secret)
|
||||
}
|
||||
return err
|
||||
}
|
||||
existing.StringData = secret.StringData
|
||||
return kube.Update(ctx, &existing)
|
||||
}
|
||||
|
||||
func kustomizationPathDiffers(ctx context.Context, kube client.Client, objKey client.ObjectKey, path string) (string, error) {
|
||||
var k kustomizev1.Kustomization
|
||||
if err := kube.Get(ctx, objKey, &k); err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
normalizePath := func(p string) string {
|
||||
return fmt.Sprintf("./%s", strings.TrimPrefix(p, "./"))
|
||||
}
|
||||
if normalizePath(path) == normalizePath(k.Spec.Path) {
|
||||
return "", nil
|
||||
}
|
||||
return k.Spec.Path, nil
|
||||
}
|
||||
|
||||
func kustomizationReconciled(ctx context.Context, kube client.Client, objKey client.ObjectKey,
|
||||
kustomization *kustomizev1.Kustomization, expectRevision string) func() (bool, error) {
|
||||
|
||||
return func() (bool, error) {
|
||||
if err := kube.Get(ctx, objKey, kustomization); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Confirm the state we are observing is for the current generation
|
||||
if kustomization.Generation != kustomization.Status.ObservedGeneration {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Confirm the given revision has been attempted by the controller
|
||||
if kustomization.Status.LastAttemptedRevision != expectRevision {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Confirm the resource is healthy
|
||||
if c := apimeta.FindStatusCondition(kustomization.Status.Conditions, meta.ReadyCondition); c != nil {
|
||||
switch c.Status {
|
||||
case metav1.ConditionTrue:
|
||||
return true, nil
|
||||
case metav1.ConditionFalse:
|
||||
return false, fmt.Errorf(c.Message)
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func retry(retries int, wait time.Duration, fn func() error) (err error) {
|
||||
for i := 0; ; i++ {
|
||||
err = fn()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if i >= retries {
|
||||
break
|
||||
}
|
||||
time.Sleep(wait)
|
||||
}
|
||||
return err
|
||||
}
|
@ -0,0 +1,330 @@
|
||||
/*
|
||||
Copyright 2021 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"sigs.k8s.io/cli-utils/pkg/object"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/kustomize/api/filesys"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
|
||||
|
||||
"github.com/fluxcd/flux2/internal/bootstrap/git"
|
||||
|
||||
"github.com/fluxcd/flux2/internal/utils"
|
||||
"github.com/fluxcd/flux2/pkg/log"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/install"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/kustomization"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/sync"
|
||||
"github.com/fluxcd/flux2/pkg/status"
|
||||
)
|
||||
|
||||
type PlainGitBootstrapper struct {
|
||||
url string
|
||||
branch string
|
||||
|
||||
author git.Author
|
||||
commitMessageAppendix string
|
||||
|
||||
kubeconfig string
|
||||
kubecontext string
|
||||
|
||||
postGenerateSecret []PostGenerateSecretFunc
|
||||
|
||||
git git.Git
|
||||
kube client.Client
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
type GitOption interface {
|
||||
applyGit(b *PlainGitBootstrapper)
|
||||
}
|
||||
|
||||
func WithRepositoryURL(url string) GitOption {
|
||||
return repositoryURLOption(url)
|
||||
}
|
||||
|
||||
type repositoryURLOption string
|
||||
|
||||
func (o repositoryURLOption) applyGit(b *PlainGitBootstrapper) {
|
||||
b.url = string(o)
|
||||
}
|
||||
|
||||
func WithPostGenerateSecretFunc(callback PostGenerateSecretFunc) GitOption {
|
||||
return postGenerateSecret(callback)
|
||||
}
|
||||
|
||||
type postGenerateSecret PostGenerateSecretFunc
|
||||
|
||||
func (o postGenerateSecret) applyGit(b *PlainGitBootstrapper) {
|
||||
b.postGenerateSecret = append(b.postGenerateSecret, PostGenerateSecretFunc(o))
|
||||
}
|
||||
|
||||
func NewPlainGitProvider(git git.Git, kube client.Client, opts ...GitOption) (*PlainGitBootstrapper, error) {
|
||||
b := &PlainGitBootstrapper{
|
||||
git: git,
|
||||
kube: kube,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.applyGit(b)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifestsBase string, options install.Options) error {
|
||||
// Clone if not already
|
||||
if _, err := b.git.Status(); err != nil {
|
||||
if err != git.ErrNoGitRepository {
|
||||
return err
|
||||
}
|
||||
|
||||
b.logger.Actionf("cloning branch %q from Git repository %q", b.branch, b.url)
|
||||
var cloned bool
|
||||
if err = retry(1, 2*time.Second, func() (err error) {
|
||||
cloned, err = b.git.Clone(ctx, b.url, b.branch)
|
||||
return
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to clone repository: %w", err)
|
||||
}
|
||||
if cloned {
|
||||
b.logger.Successf("cloned repository")
|
||||
}
|
||||
}
|
||||
|
||||
// Generate component manifests
|
||||
b.logger.Actionf("generating component manifests")
|
||||
manifests, err := install.Generate(options, manifestsBase)
|
||||
if err != nil {
|
||||
return fmt.Errorf("component manifest generation failed: %w", err)
|
||||
}
|
||||
b.logger.Successf("generated component manifests")
|
||||
|
||||
// Write manifest to Git repository
|
||||
if err = b.git.Write(manifests.Path, strings.NewReader(manifests.Content)); err != nil {
|
||||
return fmt.Errorf("failed to write manifest %q: %w", manifests.Path, err)
|
||||
}
|
||||
|
||||
// Git commit generated
|
||||
commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version)
|
||||
if b.commitMessageAppendix != "" {
|
||||
commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix
|
||||
}
|
||||
commit, err := b.git.Commit(git.Commit{
|
||||
Author: b.author,
|
||||
Message: commitMsg,
|
||||
})
|
||||
if err != nil && err != git.ErrNoStagedFiles {
|
||||
return fmt.Errorf("failed to commit sync manifests: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
b.logger.Successf("committed sync manifests to %q (%q)", b.branch, commit)
|
||||
b.logger.Actionf("pushing component manifests to %q", b.url)
|
||||
if err = b.git.Push(ctx); err != nil {
|
||||
return fmt.Errorf("failed to push manifests: %w", err)
|
||||
}
|
||||
} else {
|
||||
b.logger.Successf("component manifests are up to date")
|
||||
}
|
||||
|
||||
// Conditionally install manifests
|
||||
if mustInstallManifests(ctx, b.kube, options.Namespace) {
|
||||
b.logger.Actionf("installing components in %q namespace", options.Namespace)
|
||||
kubectlArgs := []string{"apply", "-f", filepath.Join(b.git.Path(), manifests.Path)}
|
||||
if _, err = utils.ExecKubectlCommand(ctx, utils.ModeStderrOS, b.kubeconfig, b.kubecontext, kubectlArgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
b.logger.Successf("installed components")
|
||||
}
|
||||
|
||||
b.logger.Successf("reconciled components")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *PlainGitBootstrapper) ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error {
|
||||
// Determine if there is an existing secret
|
||||
secretKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace}
|
||||
b.logger.Actionf("determining if source secret %q exists", secretKey)
|
||||
ok, err := secretExists(ctx, b.kube, secretKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine if deploy key secret exists: %w", err)
|
||||
}
|
||||
|
||||
// Return early if exists and no custom config is passed
|
||||
if ok && len(options.CAFilePath+options.PrivateKeyPath+options.Username+options.Password) == 0 {
|
||||
b.logger.Successf("source secret up to date")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate source secret
|
||||
b.logger.Actionf("generating source secret")
|
||||
manifest, err := sourcesecret.Generate(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var secret corev1.Secret
|
||||
if err := yaml.Unmarshal([]byte(manifest.Content), &secret); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal generated source secret manifest: %w", err)
|
||||
}
|
||||
|
||||
for _, callback := range b.postGenerateSecret {
|
||||
if err = callback(ctx, secret, options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Apply source secret
|
||||
b.logger.Actionf("applying source secret %q", secretKey)
|
||||
if err = reconcileSecret(ctx, b.kube, secret); err != nil {
|
||||
return err
|
||||
}
|
||||
b.logger.Successf("reconciled source secret")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error {
|
||||
// Confirm that sync configuration does not overwrite existing config
|
||||
if curPath, err := kustomizationPathDiffers(ctx, b.kube, client.ObjectKey{Name: options.Name, Namespace: options.Namespace}, options.TargetPath); err != nil {
|
||||
return fmt.Errorf("failed to determine if sync configuration would overwrite existing Kustomization: %w", err)
|
||||
} else if curPath != "" {
|
||||
return fmt.Errorf("sync path configuration (%q) would overwrite path (%q) of existing Kustomization", options.TargetPath, curPath)
|
||||
}
|
||||
|
||||
// Clone if not already
|
||||
if _, err := b.git.Status(); err != nil {
|
||||
if err == git.ErrNoGitRepository {
|
||||
b.logger.Actionf("cloning branch %q from Git repository %q", b.branch, b.url)
|
||||
var cloned bool
|
||||
if err = retry(1, 2*time.Second, func() (err error) {
|
||||
cloned, err = b.git.Clone(ctx, b.url, b.branch)
|
||||
return
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to clone repository: %w", err)
|
||||
}
|
||||
if cloned {
|
||||
b.logger.Successf("cloned repository")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate sync manifests and write to Git repository
|
||||
b.logger.Actionf("generating sync manifests")
|
||||
manifests, err := sync.Generate(options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sync manifests generation failed: %w", err)
|
||||
}
|
||||
if err = b.git.Write(manifests.Path, strings.NewReader(manifests.Content)); err != nil {
|
||||
return fmt.Errorf("failed to write manifest %q: %w", manifests.Path, err)
|
||||
}
|
||||
kusManifests, err := kustomization.Generate(kustomization.Options{
|
||||
FileSystem: filesys.MakeFsOnDisk(),
|
||||
BaseDir: b.git.Path(),
|
||||
TargetPath: filepath.Dir(manifests.Path),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("kustomization.yaml generation failed: %w", err)
|
||||
}
|
||||
if err = b.git.Write(kusManifests.Path, strings.NewReader(kusManifests.Content)); err != nil {
|
||||
return fmt.Errorf("failed to write manifest %q: %w", kusManifests.Path, err)
|
||||
}
|
||||
b.logger.Successf("generated sync manifests")
|
||||
|
||||
// Git commit generated
|
||||
commitMsg := fmt.Sprintf("Add Flux sync manifests")
|
||||
if b.commitMessageAppendix != "" {
|
||||
commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix
|
||||
}
|
||||
commit, err := b.git.Commit(git.Commit{
|
||||
Author: b.author,
|
||||
Message: commitMsg,
|
||||
})
|
||||
if err != nil && err != git.ErrNoStagedFiles {
|
||||
return fmt.Errorf("failed to commit sync manifests: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
b.logger.Successf("committed sync manifests to %q (%q)", b.branch, commit)
|
||||
b.logger.Actionf("pushing sync manifests to %q", b.url)
|
||||
if err = b.git.Push(ctx); err != nil {
|
||||
return fmt.Errorf("failed to push sync manifests: %w", err)
|
||||
}
|
||||
} else {
|
||||
b.logger.Successf("sync manifests are up to date")
|
||||
}
|
||||
|
||||
// Apply to cluster
|
||||
b.logger.Actionf("applying sync manifests")
|
||||
kubectlArgs := []string{"apply", "-k", filepath.Join(b.git.Path(), filepath.Dir(kusManifests.Path))}
|
||||
if _, err = utils.ExecKubectlCommand(ctx, utils.ModeStderrOS, b.kubeconfig, b.kubecontext, kubectlArgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
b.logger.Successf("applied sync manifests")
|
||||
|
||||
// Wait till Kustomization is reconciled
|
||||
var k kustomizev1.Kustomization
|
||||
expectRevision := fmt.Sprintf("%s/%s", options.Branch, commit)
|
||||
if err := wait.PollImmediate(pollInterval, timeout, kustomizationReconciled(
|
||||
ctx, b.kube, client.ObjectKey{Name: options.Name, Namespace: options.Namespace}, &k, expectRevision),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed waiting for Kustomization: %w", err)
|
||||
}
|
||||
|
||||
b.logger.Successf("reconciled sync configuration")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *PlainGitBootstrapper) ConfirmHealthy(ctx context.Context, install install.Options, timeout time.Duration) error {
|
||||
cfg, err := utils.KubeConfig(b.kubeconfig, b.kubecontext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checker, err := status.NewStatusChecker(cfg, 2*time.Second, timeout, b.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var components = install.Components
|
||||
components = append(components, install.ComponentsExtra...)
|
||||
|
||||
var identifiers []object.ObjMetadata
|
||||
for _, component := range components {
|
||||
identifiers = append(identifiers, object.ObjMetadata{
|
||||
Namespace: install.Namespace,
|
||||
Name: component,
|
||||
GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
|
||||
})
|
||||
}
|
||||
|
||||
b.logger.Actionf("confirming components are healthy")
|
||||
if err := checker.Assess(identifiers...); err != nil {
|
||||
return err
|
||||
}
|
||||
b.logger.Successf("all components are healthy")
|
||||
return nil
|
||||
}
|
@ -0,0 +1,546 @@
|
||||
/*
|
||||
Copyright 2021 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/fluxcd/go-git-providers/gitprovider"
|
||||
|
||||
"github.com/fluxcd/flux2/internal/bootstrap/git"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/sync"
|
||||
)
|
||||
|
||||
type GitProviderBootstrapper struct {
|
||||
*PlainGitBootstrapper
|
||||
|
||||
owner string
|
||||
repository string
|
||||
personal bool
|
||||
|
||||
description string
|
||||
defaultBranch string
|
||||
visibility string
|
||||
|
||||
teams map[string]string
|
||||
|
||||
readWriteKey bool
|
||||
|
||||
bootstrapTransportType string
|
||||
syncTransportType string
|
||||
|
||||
sshHostname string
|
||||
|
||||
provider gitprovider.Client
|
||||
}
|
||||
|
||||
func NewGitProviderBootstrapper(git git.Git, provider gitprovider.Client, kube client.Client, opts ...GitProviderOption) (*GitProviderBootstrapper, error) {
|
||||
b := &GitProviderBootstrapper{
|
||||
PlainGitBootstrapper: &PlainGitBootstrapper{
|
||||
git: git,
|
||||
kube: kube,
|
||||
},
|
||||
bootstrapTransportType: "https",
|
||||
syncTransportType: "ssh",
|
||||
provider: provider,
|
||||
}
|
||||
b.PlainGitBootstrapper.postGenerateSecret = append(b.PlainGitBootstrapper.postGenerateSecret, b.reconcileDeployKey)
|
||||
for _, opt := range opts {
|
||||
opt.applyGitProvider(b)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
type GitProviderOption interface {
|
||||
applyGitProvider(b *GitProviderBootstrapper)
|
||||
}
|
||||
|
||||
func WithProviderRepository(owner, repository string, personal bool) GitProviderOption {
|
||||
return providerRepositoryOption{
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
personal: personal,
|
||||
}
|
||||
}
|
||||
|
||||
type providerRepositoryOption struct {
|
||||
owner string
|
||||
repository string
|
||||
personal bool
|
||||
}
|
||||
|
||||
func (o providerRepositoryOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
b.owner = o.owner
|
||||
b.repository = o.repository
|
||||
b.personal = o.personal
|
||||
}
|
||||
|
||||
func WithProviderRepositoryConfig(description, defaultBranch, visibility string) GitProviderOption {
|
||||
return providerRepositoryConfigOption{
|
||||
description: description,
|
||||
defaultBranch: defaultBranch,
|
||||
visibility: visibility,
|
||||
}
|
||||
}
|
||||
|
||||
type providerRepositoryConfigOption struct {
|
||||
description string
|
||||
defaultBranch string
|
||||
visibility string
|
||||
}
|
||||
|
||||
func (o providerRepositoryConfigOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
b.description = o.description
|
||||
b.defaultBranch = o.defaultBranch
|
||||
b.visibility = o.visibility
|
||||
}
|
||||
|
||||
func WithProviderTeamPermissions(teams map[string]string) GitProviderOption {
|
||||
return providerRepositoryTeamPermissionsOption(teams)
|
||||
}
|
||||
|
||||
type providerRepositoryTeamPermissionsOption map[string]string
|
||||
|
||||
func (o providerRepositoryTeamPermissionsOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
b.teams = o
|
||||
}
|
||||
|
||||
func WithReadWriteKeyPermissions(b bool) GitProviderOption {
|
||||
return withReadWriteKeyPermissionsOption(b)
|
||||
}
|
||||
|
||||
type withReadWriteKeyPermissionsOption bool
|
||||
|
||||
func (o withReadWriteKeyPermissionsOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
b.readWriteKey = bool(o)
|
||||
}
|
||||
|
||||
func WithBootstrapTransportType(protocol string) GitProviderOption {
|
||||
return bootstrapTransportTypeOption(protocol)
|
||||
}
|
||||
|
||||
type bootstrapTransportTypeOption string
|
||||
|
||||
func (o bootstrapTransportTypeOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
b.bootstrapTransportType = string(o)
|
||||
}
|
||||
|
||||
func WithSyncTransportType(protocol string) GitProviderOption {
|
||||
return syncProtocolOption(protocol)
|
||||
}
|
||||
|
||||
type syncProtocolOption string
|
||||
|
||||
func (o syncProtocolOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
b.syncTransportType = string(o)
|
||||
}
|
||||
|
||||
func WithSSHHostname(hostname string) GitProviderOption {
|
||||
return sshHostnameOption(hostname)
|
||||
}
|
||||
|
||||
type sshHostnameOption string
|
||||
|
||||
func (o sshHostnameOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
b.sshHostname = string(o)
|
||||
}
|
||||
|
||||
func (b *GitProviderBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error {
|
||||
repo, err := b.getRepository(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if b.url == "" {
|
||||
bootstrapURL, err := b.getCloneURL(repo, gitprovider.TransportType(b.bootstrapTransportType))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
WithRepositoryURL(bootstrapURL).applyGit(b.PlainGitBootstrapper)
|
||||
}
|
||||
if options.URL == "" {
|
||||
syncURL, err := b.getCloneURL(repo, gitprovider.TransportType(b.syncTransportType))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.URL = syncURL
|
||||
}
|
||||
return b.PlainGitBootstrapper.ReconcileSyncConfig(ctx, options, pollInterval, timeout)
|
||||
}
|
||||
|
||||
// ReconcileRepository reconciles an organization or user repository with the
|
||||
// GitProviderBootstrapper configuration. On success, the URL in the embedded
|
||||
// PlainGitBootstrapper is set to clone URL for the configured protocol.
|
||||
//
|
||||
// When part of the reconciliation fails with a warning without aborting, an
|
||||
// ErrReconciledWithWarning error is returned.
|
||||
func (b *GitProviderBootstrapper) ReconcileRepository(ctx context.Context) error {
|
||||
var repo gitprovider.UserRepository
|
||||
var err error
|
||||
|
||||
if b.personal {
|
||||
repo, err = b.reconcileUserRepository(ctx)
|
||||
} else {
|
||||
repo, err = b.reconcileOrgRepository(ctx)
|
||||
}
|
||||
if err != nil && !errors.Is(err, ErrReconciledWithWarning) {
|
||||
return err
|
||||
}
|
||||
|
||||
cloneURL := repo.Repository().GetCloneURL(gitprovider.TransportType(b.bootstrapTransportType))
|
||||
// TODO(hidde): https://github.com/fluxcd/go-git-providers/issues/55
|
||||
if strings.HasPrefix(cloneURL, "https://https://") {
|
||||
cloneURL = strings.TrimPrefix(cloneURL, "https://")
|
||||
}
|
||||
WithRepositoryURL(cloneURL).applyGit(b.PlainGitBootstrapper)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *GitProviderBootstrapper) reconcileDeployKey(ctx context.Context, secret corev1.Secret, options sourcesecret.Options) error {
|
||||
ppk, ok := secret.StringData[sourcesecret.PublicKeySecretKey]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
b.logger.Successf("public key: %s", strings.TrimSpace(ppk))
|
||||
|
||||
repo, err := b.getRepository(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := deployKeyName(options.Namespace, b.branch, options.Name, options.TargetPath)
|
||||
deployKeyInfo := newDeployKeyInfo(name, ppk, b.readWriteKey)
|
||||
var changed bool
|
||||
if _, changed, err = repo.DeployKeys().Reconcile(ctx, deployKeyInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
if changed {
|
||||
b.logger.Successf("configured deploy key %q for %q", deployKeyInfo.Name, repo.Repository().String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconcileOrgRepository reconciles a gitprovider.OrgRepository
|
||||
// with the GitProviderBootstrapper values, including any
|
||||
// gitprovider.TeamAccessInfo configurations.
|
||||
//
|
||||
// If one of the gitprovider.TeamAccessInfo does not reconcile
|
||||
// successfully, the gitprovider.UserRepository and an
|
||||
// ErrReconciledWithWarning error are returned.
|
||||
func (b *GitProviderBootstrapper) reconcileOrgRepository(ctx context.Context) (gitprovider.UserRepository, error) {
|
||||
b.logger.Actionf("connecting to %s", b.provider.SupportedDomain())
|
||||
|
||||
// Construct the repository and other configuration objects
|
||||
// go-git-provider likes to work with
|
||||
subOrgs, repoName := splitSubOrganizationsFromRepositoryName(b.repository)
|
||||
orgRef := newOrganizationRef(b.provider.SupportedDomain(), b.owner, subOrgs)
|
||||
repoRef := newOrgRepositoryRef(orgRef, repoName)
|
||||
repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility)
|
||||
|
||||
// Reconcile the organization repository
|
||||
repo, err := b.provider.OrgRepositories().Get(ctx, repoRef)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gitprovider.ErrNotFound) {
|
||||
return nil, fmt.Errorf("failed to get Git repository %q: %w", repoRef.String(), err)
|
||||
}
|
||||
// go-git-providers has at present some issues with the idempotency
|
||||
// of the available Reconcile methods, and setting e.g. the default
|
||||
// branch correctly. Resort to Create with AutoInit until this has
|
||||
// been resolved.
|
||||
repo, err = b.provider.OrgRepositories().Create(ctx, repoRef, repoInfo, &gitprovider.RepositoryCreateOptions{
|
||||
AutoInit: gitprovider.BoolVar(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new Git repository %q: %w", repoRef.String(), err)
|
||||
}
|
||||
b.logger.Successf("repository %q created", repoRef.String())
|
||||
}
|
||||
|
||||
// Set default branch before calling Reconcile due to bug described
|
||||
// above.
|
||||
repoInfo.DefaultBranch = repo.Get().DefaultBranch
|
||||
var changed bool
|
||||
if err = retry(1, 2*time.Second, func() (err error) {
|
||||
repo, changed, err = b.provider.OrgRepositories().Reconcile(ctx, repoRef, repoInfo)
|
||||
return
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to reconcile Git repository %q: %w", repoRef.String(), err)
|
||||
}
|
||||
if changed {
|
||||
b.logger.Successf("repository %q reconciled", repoRef.String())
|
||||
}
|
||||
|
||||
// Build the team access config
|
||||
teamAccessInfo, err := buildTeamAccessInfo(b.teams, gitprovider.RepositoryPermissionVar(gitprovider.RepositoryPermissionMaintain))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to reconcile repository team access: %w", err)
|
||||
}
|
||||
|
||||
// Reconcile the team access config on best effort (that being:
|
||||
// record the error as a warning, but continue with the
|
||||
// reconciliation of the others)
|
||||
var warning error
|
||||
if count := len(teamAccessInfo); count > 0 {
|
||||
b.logger.Actionf("reconciling repository permissions")
|
||||
for _, i := range teamAccessInfo {
|
||||
var err error
|
||||
_, changed, err = repo.TeamAccess().Reconcile(ctx, i)
|
||||
if err != nil {
|
||||
warning = fmt.Errorf("failed to grant permissions to team: %w", ErrReconciledWithWarning)
|
||||
b.logger.Failuref("failed to grant %q permissions to %q: %w", *i.Permission, i.Name, err)
|
||||
}
|
||||
if changed {
|
||||
b.logger.Successf("granted %q permissions to %q", *i.Permission, i.Name)
|
||||
}
|
||||
}
|
||||
b.logger.Successf("reconciled repository permissions")
|
||||
}
|
||||
return repo, warning
|
||||
}
|
||||
|
||||
// reconcileUserRepository reconciles a gitprovider.UserRepository
|
||||
// with the GitProviderBootstrapper values. It returns the reconciled
|
||||
// gitprovider.UserRepository, or an error.
|
||||
func (b *GitProviderBootstrapper) reconcileUserRepository(ctx context.Context) (gitprovider.UserRepository, error) {
|
||||
b.logger.Actionf("connecting to %s", b.provider.SupportedDomain())
|
||||
|
||||
// Construct the repository and other metadata objects
|
||||
// go-git-provider likes to work with.
|
||||
_, repoName := splitSubOrganizationsFromRepositoryName(b.repository)
|
||||
userRef := newUserRef(b.provider.SupportedDomain(), b.owner)
|
||||
repoRef := newUserRepositoryRef(userRef, repoName)
|
||||
repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility)
|
||||
|
||||
repo, err := b.provider.UserRepositories().Get(ctx, repoRef)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gitprovider.ErrNotFound) {
|
||||
return nil, fmt.Errorf("failed to get Git repository %q: %w", repoRef.String(), err)
|
||||
}
|
||||
// go-git-providers has at present some issues with the idempotency
|
||||
// of the available Reconcile methods, and setting e.g. the default
|
||||
// branch correctly. Resort to Create with AutoInit until this has
|
||||
// been resolved.
|
||||
repo, err = b.provider.UserRepositories().Create(ctx, repoRef, repoInfo, &gitprovider.RepositoryCreateOptions{
|
||||
AutoInit: gitprovider.BoolVar(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new Git repository %q: %w", repoRef.String(), err)
|
||||
}
|
||||
b.logger.Successf("repository %q created", repoRef.String())
|
||||
}
|
||||
|
||||
// Set default branch before calling Reconcile due to bug described
|
||||
// above.
|
||||
repoInfo.DefaultBranch = repo.Get().DefaultBranch
|
||||
var changed bool
|
||||
if err = retry(1, 2*time.Second, func() (err error) {
|
||||
repo, changed, err = b.provider.UserRepositories().Reconcile(ctx, repoRef, repoInfo)
|
||||
return
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to reconcile Git repository %q: %w", repoRef.String(), err)
|
||||
}
|
||||
if changed {
|
||||
b.logger.Successf("repository %q reconciled", repoRef.String())
|
||||
}
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// getRepository retrieves and returns the gitprovider.UserRepository
|
||||
// for organization and user repositories using the
|
||||
// GitProviderBootstrapper values.
|
||||
// As gitprovider.OrgRepository is a superset of gitprovider.UserRepository, this
|
||||
// type is returned.
|
||||
func (b *GitProviderBootstrapper) getRepository(ctx context.Context) (gitprovider.UserRepository, error) {
|
||||
subOrgs, repoName := splitSubOrganizationsFromRepositoryName(b.repository)
|
||||
|
||||
if b.personal {
|
||||
userRef := newUserRef(b.provider.SupportedDomain(), b.owner)
|
||||
return b.provider.UserRepositories().Get(ctx, newUserRepositoryRef(userRef, repoName))
|
||||
}
|
||||
|
||||
orgRef := newOrganizationRef(b.provider.SupportedDomain(), b.owner, subOrgs)
|
||||
return b.provider.OrgRepositories().Get(ctx, newOrgRepositoryRef(orgRef, repoName))
|
||||
}
|
||||
|
||||
// getCloneURL returns the Git clone URL for the given
|
||||
// gitprovider.UserRepository. If the given transport type is
|
||||
// gitprovider.TransportTypeSSH and a custom SSH hostname is configured,
|
||||
// the hostname of the URL will be modified to this hostname.
|
||||
func (b *GitProviderBootstrapper) getCloneURL(repository gitprovider.UserRepository, transport gitprovider.TransportType) (string, error) {
|
||||
u := repository.Repository().GetCloneURL(transport)
|
||||
// TODO(hidde): https://github.com/fluxcd/go-git-providers/issues/55
|
||||
if strings.HasPrefix(u, "https://https://") {
|
||||
u = strings.TrimPrefix(u, "https://")
|
||||
}
|
||||
var err error
|
||||
if transport == gitprovider.TransportTypeSSH && b.sshHostname != "" {
|
||||
if u, err = setHostname(u, b.sshHostname); err != nil {
|
||||
err = fmt.Errorf("failed to set SSH hostname for URL %q: %w", u, err)
|
||||
}
|
||||
}
|
||||
return u, err
|
||||
}
|
||||
|
||||
// splitSubOrganizationsFromRepositoryName removes any prefixed sub
|
||||
// organizations from the given repository name by splitting the
|
||||
// string into a slice by '/'.
|
||||
// The last (or only) item of the slice result is assumed to be the
|
||||
// repository name, other items (nested) sub organizations.
|
||||
func splitSubOrganizationsFromRepositoryName(name string) ([]string, string) {
|
||||
elements := strings.Split(name, "/")
|
||||
i := len(elements)
|
||||
switch i {
|
||||
case 1:
|
||||
return nil, name
|
||||
default:
|
||||
return elements[:i-1], elements[i-1]
|
||||
}
|
||||
}
|
||||
|
||||
// buildTeamAccessInfo constructs a gitprovider.TeamAccessInfo slice
|
||||
// from the given string map of team names to permissions.
|
||||
//
|
||||
// Providing a default gitprovider.RepositoryPermission is optional,
|
||||
// and omitting it will make it default to the go-git-provider default.
|
||||
//
|
||||
// An error is returned if any of the given permissions is invalid.
|
||||
func buildTeamAccessInfo(m map[string]string, defaultPermissions *gitprovider.RepositoryPermission) ([]gitprovider.TeamAccessInfo, error) {
|
||||
var infos []gitprovider.TeamAccessInfo
|
||||
if defaultPermissions != nil {
|
||||
if err := gitprovider.ValidateRepositoryPermission(*defaultPermissions); err != nil {
|
||||
return nil, fmt.Errorf("invalid default team permission %q", *defaultPermissions)
|
||||
}
|
||||
}
|
||||
for n, p := range m {
|
||||
permission := defaultPermissions
|
||||
if p != "" {
|
||||
p := gitprovider.RepositoryPermission(p)
|
||||
if err := gitprovider.ValidateRepositoryPermission(p); err != nil {
|
||||
return nil, fmt.Errorf("invalid permission %q for team %q", p, n)
|
||||
}
|
||||
permission = &p
|
||||
}
|
||||
i := gitprovider.TeamAccessInfo{
|
||||
Name: n,
|
||||
Permission: permission,
|
||||
}
|
||||
infos = append(infos, i)
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
// newOrganizationRef constructs a gitprovider.OrganizationRef with the
|
||||
// given values and returns the result.
|
||||
func newOrganizationRef(domain, organization string, subOrganizations []string) gitprovider.OrganizationRef {
|
||||
return gitprovider.OrganizationRef{
|
||||
Domain: domain,
|
||||
Organization: organization,
|
||||
SubOrganizations: subOrganizations,
|
||||
}
|
||||
}
|
||||
|
||||
// newOrgRepositoryRef constructs a gitprovider.OrgRepositoryRef with
|
||||
// the given values and returns the result.
|
||||
func newOrgRepositoryRef(organizationRef gitprovider.OrganizationRef, name string) gitprovider.OrgRepositoryRef {
|
||||
return gitprovider.OrgRepositoryRef{
|
||||
OrganizationRef: organizationRef,
|
||||
RepositoryName: name,
|
||||
}
|
||||
}
|
||||
|
||||
// newUserRef constructs a gitprovider.UserRef with the given values
|
||||
// and returns the result.
|
||||
func newUserRef(domain, login string) gitprovider.UserRef {
|
||||
return gitprovider.UserRef{
|
||||
Domain: domain,
|
||||
UserLogin: login,
|
||||
}
|
||||
}
|
||||
|
||||
// newUserRepositoryRef constructs a gitprovider.UserRepositoryRef with
|
||||
// the given values and returns the result.
|
||||
func newUserRepositoryRef(userRef gitprovider.UserRef, name string) gitprovider.UserRepositoryRef {
|
||||
return gitprovider.UserRepositoryRef{
|
||||
UserRef: userRef,
|
||||
RepositoryName: name,
|
||||
}
|
||||
}
|
||||
|
||||
// newRepositoryInfo constructs a gitprovider.RepositoryInfo with the
|
||||
// given values and returns the result.
|
||||
func newRepositoryInfo(description, defaultBranch, visibility string) gitprovider.RepositoryInfo {
|
||||
var i gitprovider.RepositoryInfo
|
||||
if description != "" {
|
||||
i.Description = gitprovider.StringVar(description)
|
||||
}
|
||||
if defaultBranch != "" {
|
||||
i.DefaultBranch = gitprovider.StringVar(defaultBranch)
|
||||
}
|
||||
if visibility != "" {
|
||||
i.Visibility = gitprovider.RepositoryVisibilityVar(gitprovider.RepositoryVisibility(visibility))
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// newDeployKeyInfo constructs a gitprovider.DeployKeyInfo with the
|
||||
// given values and returns the result.
|
||||
func newDeployKeyInfo(name, publicKey string, readWrite bool) gitprovider.DeployKeyInfo {
|
||||
keyInfo := gitprovider.DeployKeyInfo{
|
||||
Name: name,
|
||||
Key: []byte(publicKey),
|
||||
}
|
||||
if readWrite {
|
||||
keyInfo.ReadOnly = gitprovider.BoolVar(false)
|
||||
}
|
||||
return keyInfo
|
||||
}
|
||||
|
||||
func deployKeyName(namespace, secretName, branch, path string) string {
|
||||
var name string
|
||||
for _, v := range []string{namespace, secretName, branch, path} {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if name == "" {
|
||||
name = v
|
||||
} else {
|
||||
name = name + "-" + v
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// setHostname is a helper to replace the hostname of the given URL.
|
||||
// TODO(hidde): support for this should be added in go-git-providers.
|
||||
func setHostname(URL, hostname string) (string, error) {
|
||||
u, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return URL, err
|
||||
}
|
||||
u.Host = hostname
|
||||
return u.String(), nil
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright 2021 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoGitRepository = errors.New("no git repository")
|
||||
ErrNoStagedFiles = errors.New("no staged files")
|
||||
)
|
||||
|
||||
type Author struct {
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
Author
|
||||
Hash string
|
||||
Message string
|
||||
}
|
||||
|
||||
// Git is an interface for basic Git operations on a single branch of a
|
||||
// remote repository.
|
||||
type Git interface {
|
||||
Init(url, branch string) (bool, error)
|
||||
Clone(ctx context.Context, url, branch string) (bool, error)
|
||||
Write(path string, reader io.Reader) error
|
||||
Commit(message Commit) (string, error)
|
||||
Push(ctx context.Context) error
|
||||
Status() (bool, error)
|
||||
Path() string
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
/*
|
||||
Copyright 2021 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package gogit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
|
||||
"github.com/fluxcd/flux2/internal/bootstrap/git"
|
||||
)
|
||||
|
||||
type GoGit struct {
|
||||
path string
|
||||
auth transport.AuthMethod
|
||||
repository *gogit.Repository
|
||||
}
|
||||
|
||||
func New(path string, auth transport.AuthMethod) *GoGit {
|
||||
return &GoGit{
|
||||
path: path,
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GoGit) Init(url, branch string) (bool, error) {
|
||||
if g.repository != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
r, err := gogit.PlainInit(g.path, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if _, err = r.CreateRemote(&config.RemoteConfig{
|
||||
Name: gogit.DefaultRemoteName,
|
||||
URLs: []string{url},
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
branchRef := plumbing.NewBranchReferenceName(branch)
|
||||
if err = r.CreateBranch(&config.Branch{
|
||||
Name: branch,
|
||||
Remote: gogit.DefaultRemoteName,
|
||||
Merge: branchRef,
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
// PlainInit assumes the initial branch to always be master, we can
|
||||
// overwrite this by setting the reference of the Storer to a new
|
||||
// symbolic reference (as there are no commits yet) that points
|
||||
// the HEAD to our new branch.
|
||||
if err = r.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, branchRef)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
g.repository = r
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (g *GoGit) Clone(ctx context.Context, url, branch string) (bool, error) {
|
||||
branchRef := plumbing.NewBranchReferenceName(branch)
|
||||
r, err := gogit.PlainCloneContext(ctx, g.path, false, &gogit.CloneOptions{
|
||||
URL: url,
|
||||
Auth: g.auth,
|
||||
RemoteName: gogit.DefaultRemoteName,
|
||||
ReferenceName: branchRef,
|
||||
SingleBranch: true,
|
||||
|
||||
NoCheckout: false,
|
||||
Progress: nil,
|
||||
Tags: gogit.NoTags,
|
||||
})
|
||||
if err != nil {
|
||||
if err == transport.ErrEmptyRemoteRepository || isRemoteBranchNotFoundErr(err, branchRef.String()) {
|
||||
return g.Init(url, branch)
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
g.repository = r
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (g *GoGit) Write(path string, reader io.Reader) error {
|
||||
if g.repository == nil {
|
||||
return git.ErrNoGitRepository
|
||||
}
|
||||
|
||||
wt, err := g.repository.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := wt.Filesystem.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.Copy(f, reader)
|
||||
return err
|
||||
}
|
||||
|
||||
func (g *GoGit) Commit(message git.Commit) (string, error) {
|
||||
if g.repository == nil {
|
||||
return "", git.ErrNoGitRepository
|
||||
}
|
||||
|
||||
wt, err := g.repository.Worktree()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
status, err := wt.Status()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// go-git has [a bug](https://github.com/go-git/go-git/issues/253)
|
||||
// whereby it thinks broken symlinks to absolute paths are
|
||||
// modified. There's no circumstance in which we want to commit a
|
||||
// change to a broken symlink: so, detect and skip those.
|
||||
var changed bool
|
||||
for file, _ := range status {
|
||||
abspath := filepath.Join(g.path, file)
|
||||
info, err := os.Lstat(abspath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("checking if %s is a symlink: %w", file, err)
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink > 0 {
|
||||
// symlinks are OK; broken symlinks are probably a result
|
||||
// of the bug mentioned above, but not of interest in any
|
||||
// case.
|
||||
if _, err := os.Stat(abspath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
_, _ = wt.Add(file)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if !changed {
|
||||
head, err := g.repository.Head()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return head.Hash().String(), git.ErrNoStagedFiles
|
||||
}
|
||||
|
||||
commit, err := wt.Commit(message.Message, &gogit.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: message.Name,
|
||||
Email: message.Email,
|
||||
When: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return commit.String(), nil
|
||||
}
|
||||
|
||||
func (g *GoGit) Push(ctx context.Context) error {
|
||||
if g.repository == nil {
|
||||
return git.ErrNoGitRepository
|
||||
}
|
||||
|
||||
return g.repository.PushContext(ctx, &gogit.PushOptions{
|
||||
RemoteName: gogit.DefaultRemoteName,
|
||||
Auth: g.auth,
|
||||
Progress: nil,
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GoGit) Status() (bool, error) {
|
||||
if g.repository == nil {
|
||||
return false, git.ErrNoGitRepository
|
||||
}
|
||||
wt, err := g.repository.Worktree()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
status, err := wt.Status()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return status.IsClean(), nil
|
||||
}
|
||||
|
||||
func (g *GoGit) Path() string {
|
||||
return g.path
|
||||
}
|
||||
|
||||
func isRemoteBranchNotFoundErr(err error, ref string) bool {
|
||||
return strings.Contains(err.Error(), fmt.Sprintf("couldn't find remote ref %q", ref))
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2021 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/fluxcd/flux2/internal/bootstrap/git"
|
||||
"github.com/fluxcd/flux2/pkg/log"
|
||||
)
|
||||
|
||||
type Option interface {
|
||||
GitOption
|
||||
GitProviderOption
|
||||
}
|
||||
|
||||
func WithBranch(branch string) Option {
|
||||
return branchOption(branch)
|
||||
}
|
||||
|
||||
type branchOption string
|
||||
|
||||
func (o branchOption) applyGit(b *PlainGitBootstrapper) {
|
||||
b.branch = string(o)
|
||||
}
|
||||
|
||||
func (o branchOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
o.applyGit(b.PlainGitBootstrapper)
|
||||
}
|
||||
|
||||
func WithAuthor(name, email string) Option {
|
||||
return authorOption{
|
||||
Name: name,
|
||||
Email: email,
|
||||
}
|
||||
}
|
||||
|
||||
type authorOption git.Author
|
||||
|
||||
func (o authorOption) applyGit(b *PlainGitBootstrapper) {
|
||||
if o.Name != "" {
|
||||
b.author.Name = o.Name
|
||||
}
|
||||
if o.Email != "" {
|
||||
b.author.Email = o.Email
|
||||
}
|
||||
}
|
||||
|
||||
func (o authorOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
o.applyGit(b.PlainGitBootstrapper)
|
||||
}
|
||||
|
||||
func WithCommitMessageAppendix(appendix string) Option {
|
||||
return commitMessageAppendixOption(appendix)
|
||||
}
|
||||
|
||||
type commitMessageAppendixOption string
|
||||
|
||||
func (o commitMessageAppendixOption) applyGit(b *PlainGitBootstrapper) {
|
||||
b.commitMessageAppendix = string(o)
|
||||
}
|
||||
|
||||
func (o commitMessageAppendixOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
o.applyGit(b.PlainGitBootstrapper)
|
||||
}
|
||||
|
||||
func WithKubeconfig(kubeconfig, kubecontext string) Option {
|
||||
return kubeconfigOption{
|
||||
kubeconfig: kubeconfig,
|
||||
kubecontext: kubecontext,
|
||||
}
|
||||
}
|
||||
|
||||
type kubeconfigOption struct {
|
||||
kubeconfig string
|
||||
kubecontext string
|
||||
}
|
||||
|
||||
func (o kubeconfigOption) applyGit(b *PlainGitBootstrapper) {
|
||||
b.kubeconfig = o.kubeconfig
|
||||
b.kubecontext = o.kubecontext
|
||||
}
|
||||
|
||||
func (o kubeconfigOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
o.applyGit(b.PlainGitBootstrapper)
|
||||
}
|
||||
|
||||
func WithLogger(logger log.Logger) Option {
|
||||
return loggerOption{logger}
|
||||
}
|
||||
|
||||
type loggerOption struct {
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (o loggerOption) applyGit(b *PlainGitBootstrapper) {
|
||||
b.logger = o.logger
|
||||
}
|
||||
|
||||
func (o loggerOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
b.logger = o.logger
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright 2021 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fluxcd/go-git-providers/github"
|
||||
"github.com/fluxcd/go-git-providers/gitlab"
|
||||
"github.com/fluxcd/go-git-providers/gitprovider"
|
||||
)
|
||||
|
||||
// BuildGitProvider builds a gitprovider.Client for the provided
|
||||
// Config. It returns an error if the Config.Provider
|
||||
// is not supported, or if the construction of the client fails.
|
||||
func BuildGitProvider(config Config) (gitprovider.Client, error) {
|
||||
var client gitprovider.Client
|
||||
var err error
|
||||
switch config.Provider {
|
||||
case GitProviderGitHub:
|
||||
opts := []github.ClientOption{
|
||||
github.WithOAuth2Token(config.Token),
|
||||
}
|
||||
if config.Hostname != "" {
|
||||
opts = append(opts, github.WithDomain(config.Hostname))
|
||||
}
|
||||
if client, err = github.NewClient(opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case GitProviderGitLab:
|
||||
opts := []gitlab.ClientOption{
|
||||
gitlab.WithConditionalRequests(true),
|
||||
}
|
||||
if config.Hostname != "" {
|
||||
opts = append(opts, gitlab.WithDomain(config.Hostname))
|
||||
}
|
||||
if client, err = gitlab.NewClient(config.Token, "", opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported Git provider '%s'", config.Provider)
|
||||
}
|
||||
return client, err
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright 2021 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package provider
|
||||
|
||||
// GitProvider holds a Git provider definition.
|
||||
type GitProvider string
|
||||
|
||||
const (
|
||||
GitProviderGitHub GitProvider = "github"
|
||||
GitProviderGitLab GitProvider = "gitlab"
|
||||
)
|
||||
|
||||
// Config defines the configuration for connecting to a GitProvider.
|
||||
type Config struct {
|
||||
// Provider defines the GitProvider.
|
||||
Provider GitProvider
|
||||
|
||||
// Hostname is the HTTP/S hostname of the Provider,
|
||||
// e.g. github.example.com.
|
||||
Hostname string
|
||||
|
||||
// Token contains the token used to authenticate with the
|
||||
// Provider.
|
||||
Token string
|
||||
}
|
Loading…
Reference in New Issue