Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions internal/controller/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ const (
// preflightFailedRequeueAfter is how long to wait before trying to reconcile
// if some preflight check has failed.
preflightFailedRequeueAfter = 30 * time.Second

httpsScheme = "https"
githubDomain = "github.com"
gitlabHostPrefix = "gitlab."
gitlabPackagesAPIPrefix = "/api/v4/projects/"
)
44 changes: 41 additions & 3 deletions internal/controller/phases.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package controller
import (
"context"
"fmt"
"net/url"
"strings"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -135,13 +137,13 @@ func (p *phaseReconciler) load(ctx context.Context) (reconcile.Result, error) {
spec := p.provider.GetSpec()

// If a configmap selector was specified, use it to find the configmap with provider configuration. This is
// a case for "air-gapped" environments. If no selector was specified, use GitHub repository.
// a case for "air-gapped" environments. If no selector was specified, use GitHub/Gitlab repository.
if spec.FetchConfig != nil && spec.FetchConfig.Selector != nil {
log.V(5).Info("Custom ConfigMap was provided for fetching manifests")

p.repo, err = p.configmapRepository(ctx)
} else {
p.repo, err = repository.NewGitHubRepository(p.providerConfig, p.configClient.Variables())
p.repo, err = repositoryFactory(p.providerConfig, p.configClient.Variables())
}

if err != nil {
Expand Down Expand Up @@ -295,7 +297,7 @@ func (p *phaseReconciler) fetch(ctx context.Context) (reconcile.Result, error) {
log := ctrl.LoggerFrom(ctx)
log.Info("Fetching provider")

// Fetch the provider components yaml file from the provided repository Github/ConfigMap.
// Fetch the provider components yaml file from the provided repository GitHub/GitLab/ConfigMap.
componentsFile, err := p.repo.GetFile(p.options.Version, p.repo.ComponentsPath())
if err != nil {
err = fmt.Errorf("failed to read %q from provider's repository %q: %w", p.repo.ComponentsPath(), p.providerConfig.ManifestLabel(), err)
Expand Down Expand Up @@ -444,3 +446,39 @@ func (s *phaseReconciler) newClusterClient() cluster.Client {
ctrlConfig: s.ctrlConfig,
}))
}

// repositoryFactory returns the repository implementation corresponding to the provider URL.
// inspired by https://github.com/kubernetes-sigs/cluster-api/blob/124d9be7035e492f027cdc7a701b6b179451190a/cmd/clusterctl/client/repository/client.go#L170
func repositoryFactory(providerConfig configclient.Provider, configVariablesClient configclient.VariablesClient) (repository.Repository, error) {
// parse the repository url
rURL, err := url.Parse(providerConfig.URL())
if err != nil {
return nil, fmt.Errorf("failed to parse repository url %q", providerConfig.URL())
}

if rURL.Scheme != httpsScheme {
return nil, fmt.Errorf("invalid provider url. there are no provider implementation for %q schema", rURL.Scheme)
}

// if the url is a GitHub repository
if rURL.Host == githubDomain {
repo, err := repository.NewGitHubRepository(providerConfig, configVariablesClient)
if err != nil {
return nil, fmt.Errorf("error creating the GitHub repository client: %w", err)
}

return repo, err
}

// if the url is a GitLab repository
if strings.HasPrefix(rURL.Host, gitlabHostPrefix) && strings.HasPrefix(rURL.RawPath, gitlabPackagesAPIPrefix) {
repo, err := repository.NewGitLabRepository(providerConfig, configVariablesClient)
if err != nil {
return nil, fmt.Errorf("error creating the GitLab repository client: %w", err)
}

return repo, err
}

return nil, fmt.Errorf("invalid provider url. Only GitHub and GitLab are supported for %q schema", rURL.Scheme)
}
85 changes: 83 additions & 2 deletions internal/controller/phases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ import (
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha1"
"sigs.k8s.io/cluster-api-operator/internal/controller/genericprovider"
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
configclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha1"
"sigs.k8s.io/cluster-api-operator/internal/controller/genericprovider"
)

func TestSecretReader(t *testing.T) {
Expand Down Expand Up @@ -358,3 +361,81 @@ metadata:
})
}
}

func TestRepositoryFactory(t *testing.T) {
testCases := []struct {
name string
fetchURL string
expectedError bool
}{
{
name: "github repo",
fetchURL: "https://github.com/kubernetes-sigs/cluster-api-provider-aws/releases/v1.4.1/infrastructure-components.yaml",
},
{
name: "gitlab repo",
fetchURL: "https://gitlab.example.org/api/v4/projects/group%2Fproject/packages/generic/cluster-api-proviver-aws/v1.4.1/path",
},
{
name: "unsupported url",
fetchURL: "https://unsupported.xyz/kubernetes-sigs/cluster-api-provider-aws/releases/v1.4.1/infrastructure-components.yaml",
expectedError: true,
},
{
name: "unsupported schema",
fetchURL: "ftp://github.com/kubernetes-sigs/cluster-api-provider-aws/releases/v1.4.1/infrastructure-components.yaml",
expectedError: true,
},
{
name: "not an url",
fetchURL: "INVALID_URL",
expectedError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)

mr := configclient.NewMemoryReader()

g.Expect(mr.Init("")).To(Succeed())

var configClient configclient.Client

var err error

providerName := "aws"
providerType := clusterctlv1.InfrastructureProviderType

// Initialize a client for interacting with the clusterctl configuration.
// Inject a provider with custom URL.
if tc.fetchURL != "" {
reader, err := mr.AddProvider(providerName, providerType, tc.fetchURL)
g.Expect(err).ToNot(HaveOccurred())

configClient, err = configclient.New("", configclient.InjectReader(reader))
g.Expect(err).ToNot(HaveOccurred())
} else {
configClient, err = configclient.New("")
g.Expect(err).ToNot(HaveOccurred())
}

// Get returns the configuration for the provider with a given name/type.
// This is done using clusterctl internal API types.
providerConfig, err := configClient.Providers().Get(providerName, providerType)
g.Expect(err).ToNot(HaveOccurred())

repo, err := repositoryFactory(providerConfig, configClient.Variables())
if tc.expectedError {
g.Expect(err).To(HaveOccurred())

return
} else {
g.Expect(err).ToNot(HaveOccurred())
}

g.Expect(repo.GetVersions()).To(ContainElement("v1.4.1"))
})
}
}