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
433 changes: 413 additions & 20 deletions cmd/plugin/cmd/init.go

Large diffs are not rendered by default.

530 changes: 530 additions & 0 deletions cmd/plugin/cmd/init_test.go

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions cmd/plugin/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,26 @@ import (
"os"
"strings"

logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
ctrl "sigs.k8s.io/controller-runtime"

"github.com/MakeNowJust/heredoc"
goerrors "github.com/go-errors/errors"
"github.com/go-logr/logr"
"github.com/spf13/cobra"
)

const (
groupDebug = "group-debug"
groupManagement = "group-management"
groupOther = "group-other"
latestVersion = "latest"
)

var verbosity *int

var log logr.Logger

// RootCmd is capioperator root CLI command.
var RootCmd = &cobra.Command{
Use: "capioperator",
Expand Down Expand Up @@ -67,6 +74,10 @@ func init() {

verbosity = flag.CommandLine.Int("v", 0, "Set the log level verbosity. This overrides the CAPIOPERATOR_LOG_LEVEL environment variable.")

log = logf.NewLogger(logf.WithThreshold(verbosity))
logf.SetLogger(log)
ctrl.SetLogger(log)

RootCmd.PersistentFlags().AddGoFlagSet(flag.CommandLine)

RootCmd.AddGroup(
Expand Down
60 changes: 60 additions & 0 deletions cmd/plugin/cmd/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright 2023 The Kubernetes 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 cmd

import (
"fmt"
"os"
"testing"
"time"

"sigs.k8s.io/cluster-api-operator/internal/envtest"
ctrl "sigs.k8s.io/controller-runtime"
)

const (
waitShort = time.Second * 5
waitLong = time.Second * 20
)

var (
env *envtest.Environment
ctx = ctrl.SetupSignalHandler()
)

func TestMain(m *testing.M) {
fmt.Println("Creating new test environment")

env = envtest.New()

go func() {
if err := env.Start(ctx); err != nil {
panic(fmt.Sprintf("Failed to start the envtest manager: %v", err))
}
}()
<-env.Manager.Elected()

// Run tests
code := m.Run()
// Tearing down the test environment
if err := env.Stop(); err != nil {
panic(fmt.Sprintf("Failed to stop the envtest: %v", err))
}

// Report exit code
os.Exit(code)
}
110 changes: 110 additions & 0 deletions cmd/plugin/cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,32 @@ limitations under the License.
package cmd

import (
"context"
"errors"
"fmt"
"os"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/clientcmd"
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"

operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
)

var capiOperatorLabels = map[string]string{
"clusterctl.cluster.x-k8s.io/core": "capi-operator",
"control-plane": "controller-manager",
}

var ErrNotFound = fmt.Errorf("resource was not found")

// CreateKubeClient creates a kubernetes client from provided kubeconfig and kubecontext.
func CreateKubeClient(kubeconfigPath, kubeconfigContext string) (ctrlclient.Client, error) {
// Use specified kubeconfig path and context
Expand All @@ -51,3 +66,98 @@ func CreateKubeClient(kubeconfigPath, kubeconfigContext string) (ctrlclient.Clie

return client, nil
}

func EnsureNamespaceExists(ctx context.Context, client ctrlclient.Client, namespace string) error {
// Check if the namespace exists
ns := &corev1.Namespace{}

err := client.Get(ctx, ctrlclient.ObjectKey{Name: namespace}, ns)
if err == nil {
return nil
}

if !apierrors.IsNotFound(err) {
return fmt.Errorf("unexpected error during namespace checking: %w", err)
}

// Create the namespace if it doesn't exist
newNamespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
},
}

if err := client.Create(ctx, newNamespace); err != nil {
return fmt.Errorf("unable to create namespace %s: %w", namespace, err)
}

return nil
}

// GetDeploymentByLabels fetches deployment based on the provided labels.
func GetDeploymentByLabels(ctx context.Context, client ctrlclient.Client, labels map[string]string) (*appsv1.Deployment, error) {
var deploymentList appsv1.DeploymentList

// Search deployments with desired labels in all namespaces.
if err := client.List(ctx, &deploymentList, ctrlclient.MatchingLabels(labels)); err != nil {
return nil, fmt.Errorf("cannot get a list of deployments from the server: %w", err)
}

if len(deploymentList.Items) > 1 {
return nil, fmt.Errorf("more than one deployment found for given labels %v", labels)
}

if len(deploymentList.Items) == 0 {
return nil, ErrNotFound
}

return &deploymentList.Items[0], nil
}

// CheckDeploymentAvailability checks if the deployment with given labels is available.
func CheckDeploymentAvailability(ctx context.Context, client ctrlclient.Client, labels map[string]string) (bool, error) {
deployment, err := GetDeploymentByLabels(ctx, client, labels)
if err != nil {
if errors.Is(err, ErrNotFound) {
return false, nil
}

return false, err
}

for _, cond := range deployment.Status.Conditions {
if cond.Type == appsv1.DeploymentAvailable && cond.Status == corev1.ConditionTrue {
return true, nil
}
}

return false, nil
}

// GetKubeconfigLocation will read the environment variable $KUBECONFIG otherwise set it to ~/.kube/config.
func GetKubeconfigLocation() string {
if kubeconfig := os.Getenv("KUBECONFIG"); kubeconfig != "" {
return kubeconfig
}

return clientcmd.RecommendedHomeFile
}

func NewGenericProvider(providerType clusterctlv1.ProviderType) operatorv1.GenericProvider {
switch providerType {
case clusterctlv1.CoreProviderType:
return &operatorv1.CoreProvider{}
case clusterctlv1.BootstrapProviderType:
return &operatorv1.BootstrapProvider{}
case clusterctlv1.ControlPlaneProviderType:
return &operatorv1.ControlPlaneProvider{}
case clusterctlv1.InfrastructureProviderType:
return &operatorv1.InfrastructureProvider{}
case clusterctlv1.AddonProviderType:
return &operatorv1.AddonProvider{}
case clusterctlv1.IPAMProviderType, clusterctlv1.RuntimeExtensionProviderType, clusterctlv1.ProviderTypeUnknown:
panic(fmt.Sprintf("unsupported provider type %s", providerType))
default:
panic(fmt.Sprintf("unknown provider type %s", providerType))
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/MakeNowJust/heredoc v1.0.0
github.com/evanphx/json-patch/v5 v5.7.0
github.com/go-errors/errors v1.5.1
github.com/go-logr/logr v1.3.0
github.com/google/go-cmp v0.6.0
github.com/google/go-github/v52 v52.0.0
github.com/google/gofuzz v1.2.0
Expand Down Expand Up @@ -51,7 +52,6 @@ require (
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
Expand Down
7 changes: 2 additions & 5 deletions internal/controller/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ const (
// if some preflight check has failed.
preflightFailedRequeueAfter = 30 * time.Second

httpsScheme = "https"
githubDomain = "github.com"
gitlabHostPrefix = "gitlab."
gitlabPackagesAPIPrefix = "/api/v4/projects/"
configPath = "/config/clusterctl.yaml"
// configPath is the path to the clusterctl config file.
configPath = "/config/clusterctl.yaml"
)
7 changes: 5 additions & 2 deletions internal/controller/manifests_downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"

ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
"sigs.k8s.io/cluster-api-operator/util"
)

const (
Expand Down Expand Up @@ -76,7 +79,7 @@ func (p *phaseReconciler) downloadManifests(ctx context.Context) (reconcile.Resu

log.Info("Downloading provider manifests")

repo, err := repositoryFactory(ctx, p.providerConfig, p.configClient.Variables())
repo, err := util.RepositoryFactory(ctx, p.providerConfig, p.configClient.Variables())
if err != nil {
err = fmt.Errorf("failed to create repo from provider url for provider %q: %w", p.provider.GetName(), err)

Expand Down
38 changes: 0 additions & 38 deletions internal/controller/phases.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ import (
"context"
"fmt"
"io"
"net/url"
"os"
"strings"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -554,42 +552,6 @@ func (p *phaseReconciler) newClusterClient() cluster.Client {
}))
}

// 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(ctx context.Context, 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these constants can be removed now


	httpsScheme             = "https"
	githubDomain            = "github.com"
	gitlabHostPrefix        = "gitlab."
	gitlabPackagesAPIPrefix = "/api/v4/projects/"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

repo, err := repository.NewGitHubRepository(ctx, 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.Path, 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)
}

func getLatestVersion(repoVersions []string) (string, error) {
if len(repoVersions) == 0 {
err := fmt.Errorf("no versions available")
Expand Down
3 changes: 2 additions & 1 deletion internal/controller/phases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/fake"

operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
"sigs.k8s.io/cluster-api-operator/util"
)

func TestSecretReader(t *testing.T) {
Expand Down Expand Up @@ -475,7 +476,7 @@ func TestRepositoryFactory(t *testing.T) {
providerConfig, err := configClient.Providers().Get(providerName, providerType)
g.Expect(err).ToNot(HaveOccurred())

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

Expand Down
2 changes: 2 additions & 0 deletions internal/envtest/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ func New(uncachedObjs ...client.Object) *Environment {
root := path.Join(path.Dir(filename), "..", "..")
crdPaths := []string{
filepath.Join(root, "config", "crd", "bases"),
// cert-manager CRDs are stored there.
filepath.Join(root, "test", "testdata"),
}

if capiPath := getFilePathToClusterctlCRDs(root); capiPath != "" {
Expand Down
Loading