Move bootstrap package from internal to pkg
Signed-off-by: Philip Laine <philip.laine@gmail.com>
This commit is contained in:
565
pkg/bootstrap/bootstrap_provider.go
Normal file
565
pkg/bootstrap/bootstrap_provider.go
Normal file
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
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/pkg/bootstrap/git"
|
||||
"github.com/fluxcd/flux2/pkg/bootstrap/provider"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
|
||||
"github.com/fluxcd/flux2/pkg/manifestgen/sync"
|
||||
)
|
||||
|
||||
type GitProviderBootstrapper struct {
|
||||
*PlainGitBootstrapper
|
||||
|
||||
owner string
|
||||
repositoryName string
|
||||
repository gitprovider.UserRepository
|
||||
|
||||
personal bool
|
||||
|
||||
description string
|
||||
defaultBranch string
|
||||
visibility string
|
||||
|
||||
reconcile bool
|
||||
|
||||
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, repositoryName string, personal bool) GitProviderOption {
|
||||
return providerRepositoryOption{
|
||||
owner: owner,
|
||||
repositoryName: repositoryName,
|
||||
personal: personal,
|
||||
}
|
||||
}
|
||||
|
||||
type providerRepositoryOption struct {
|
||||
owner string
|
||||
repositoryName string
|
||||
personal bool
|
||||
}
|
||||
|
||||
func (o providerRepositoryOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
b.owner = o.owner
|
||||
b.repositoryName = o.repositoryName
|
||||
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 WithReconcile() GitProviderOption {
|
||||
return reconcileOption(true)
|
||||
}
|
||||
|
||||
type reconcileOption bool
|
||||
|
||||
func (o reconcileOption) applyGitProvider(b *GitProviderBootstrapper) {
|
||||
b.reconcile = true
|
||||
}
|
||||
|
||||
func (b *GitProviderBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options) error {
|
||||
if b.repository == nil {
|
||||
return errors.New("repository is required")
|
||||
}
|
||||
|
||||
if b.url == "" {
|
||||
bootstrapURL, err := b.getCloneURL(b.repository, gitprovider.TransportType(b.bootstrapTransportType))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
WithRepositoryURL(bootstrapURL).applyGit(b.PlainGitBootstrapper)
|
||||
}
|
||||
if options.URL == "" {
|
||||
syncURL, err := b.getCloneURL(b.repository, gitprovider.TransportType(b.syncTransportType))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options.URL = syncURL
|
||||
}
|
||||
|
||||
return b.PlainGitBootstrapper.ReconcileSyncConfig(ctx, options)
|
||||
}
|
||||
|
||||
// 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, err := b.getCloneURL(repo, gitprovider.TransportType(b.bootstrapTransportType))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.repository = repo
|
||||
WithRepositoryURL(cloneURL).applyGit(b.PlainGitBootstrapper)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *GitProviderBootstrapper) reconcileDeployKey(ctx context.Context, secret corev1.Secret, options sourcesecret.Options) error {
|
||||
if b.repository == nil {
|
||||
return errors.New("repository is required")
|
||||
}
|
||||
|
||||
ppk, ok := secret.StringData[sourcesecret.PublicKeySecretKey]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
b.logger.Successf("public key: %s", strings.TrimSpace(ppk))
|
||||
|
||||
name := deployKeyName(options.Namespace, b.branch, options.Name, options.TargetPath)
|
||||
deployKeyInfo := newDeployKeyInfo(name, ppk, b.readWriteKey)
|
||||
|
||||
_, changed, err := b.repository.DeployKeys().Reconcile(ctx, deployKeyInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if changed {
|
||||
b.logger.Successf("configured deploy key %q for %q", deployKeyInfo.Name, b.repository.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.repositoryName)
|
||||
orgRef, err := b.getOrganization(ctx, subOrgs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new Git repository %q: %w", b.repositoryName, err)
|
||||
}
|
||||
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 until this has been resolved.
|
||||
repo, err = b.provider.OrgRepositories().Create(ctx, repoRef, repoInfo)
|
||||
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())
|
||||
}
|
||||
|
||||
var changed bool
|
||||
if b.reconcile {
|
||||
if err = retry(1, 2*time.Second, func() (err error) {
|
||||
changed, err = repo.Reconcile(ctx)
|
||||
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
|
||||
// Don't reconcile team if team already exists and b.reconcile is false
|
||||
if team, err := repo.TeamAccess().Get(ctx, i.Name); err == nil && !b.reconcile && team != nil {
|
||||
continue
|
||||
}
|
||||
_, 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: %s", *i.Permission, i.Name, err.Error())
|
||||
} else 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.repositoryName)
|
||||
userRef := newUserRef(b.provider.SupportedDomain(), b.owner)
|
||||
repoRef := newUserRepositoryRef(userRef, repoName)
|
||||
repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility)
|
||||
|
||||
// Reconcile the user repository
|
||||
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 until this has been resolved.
|
||||
repo, err = b.provider.UserRepositories().Create(ctx, repoRef, repoInfo)
|
||||
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())
|
||||
}
|
||||
|
||||
if b.reconcile {
|
||||
var changed bool
|
||||
if err = retry(1, 2*time.Second, func() (err error) {
|
||||
changed, err = repo.Reconcile(ctx)
|
||||
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
|
||||
}
|
||||
|
||||
// getOrganization retrieves and returns the gitprovider.Organization
|
||||
// using the GitProviderBootstrapper values.
|
||||
func (b *GitProviderBootstrapper) getOrganization(ctx context.Context, subOrgs []string) (*gitprovider.OrganizationRef, error) {
|
||||
orgRef := newOrganizationRef(b.provider.SupportedDomain(), b.owner, subOrgs)
|
||||
// With Stash get the organization to be sure to get the correct key
|
||||
if string(b.provider.ProviderID()) == string(provider.GitProviderStash) {
|
||||
org, err := b.provider.Organizations().Get(ctx, orgRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Git organization: %w", err)
|
||||
}
|
||||
|
||||
orgRef = org.Organization()
|
||||
|
||||
return &orgRef, nil
|
||||
}
|
||||
return &orgRef, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
var url string
|
||||
if cloner, ok := repository.(gitprovider.CloneableURL); ok {
|
||||
url = cloner.GetCloneURL("", transport)
|
||||
} else {
|
||||
url = repository.Repository().GetCloneURL(transport)
|
||||
}
|
||||
|
||||
var err error
|
||||
if transport == gitprovider.TransportTypeSSH && b.sshHostname != "" {
|
||||
if url, err = setHostname(url, b.sshHostname); err != nil {
|
||||
err = fmt.Errorf("failed to set SSH hostname for URL %q: %w", url, err)
|
||||
}
|
||||
}
|
||||
return url, 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
|
||||
}
|
||||
Reference in New Issue
Block a user