Skip to content

Commit

Permalink
Validate Templates with external source
Browse files Browse the repository at this point in the history
Also:
1. Consider template type from spec as well
2. Adapt tests

HMC-108
  • Loading branch information
eromanova committed Jul 29, 2024
1 parent cc966c5 commit 031b100
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 23 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
kubeconfig
charts/
external-crds/
# Binaries for programs and plugins
bin
dist
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,15 @@ vet: ## Run go vet against code.
tidy:
go mod tidy

EXTERNAL_CRDS_DIR := external-crds

.PHONY: external-crds
external-crds:
@mkdir -p $(EXTERNAL_CRDS_DIR)
@curl -sL https://github.com/fluxcd/source-controller/releases/download/v1.3.0/source-controller.crds.yaml > $(EXTERNAL_CRDS_DIR)/source-controller.crds.yaml

.PHONY: test
test: generate-all fmt vet envtest tidy ## Run tests.
test: generate-all fmt vet envtest tidy external-crds ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out

# Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors.
Expand Down
5 changes: 4 additions & 1 deletion internal/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ var _ = BeforeSuite(func() {

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "config", "crd", "bases"),
filepath.Join("..", "..", "external-crds"),
},
ErrorIfCRDPathMissing: true,

// The BinaryAssetsDirectory is only required if you want to run the tests directly
Expand Down
78 changes: 62 additions & 16 deletions internal/controller/template_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import (
"strings"
"time"

helmcontrollerv2 "github.com/fluxcd/helm-controller/api/v2"
v2 "github.com/fluxcd/helm-controller/api/v2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"helm.sh/helm/v3/pkg/chart"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
Expand All @@ -51,7 +53,8 @@ var (
// TemplateReconciler reconciles a Template object
type TemplateReconciler struct {
client.Client
Scheme *runtime.Scheme
Scheme *runtime.Scheme
downloadHelmChartFunc func(context.Context, *sourcev1.Artifact) (*chart.Chart, error)
}

// +kubebuilder:rbac:groups=hmc.mirantis.com,resources=templates,verbs=get;list;watch;create;update;patch;delete
Expand All @@ -72,19 +75,35 @@ func (r *TemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
l.Error(err, "Failed to get Template")
return ctrl.Result{}, err
}
l.Info("Reconciling helm-controller objects ")
hcChart, err := r.reconcileHelmChart(ctx, template)
if err != nil {
l.Error(err, "Failed to reconcile HelmChart")
return ctrl.Result{}, err

var hcChart *sourcev1.HelmChart
var err error
if template.Spec.Helm.ChartRef != nil {
hcChart, err = r.getHelmChartFromChartRef(ctx, template.Spec.Helm.ChartRef)
if err != nil {
l.Error(err, "failed to get artifact from chartRef", "kind", template.Spec.Helm.ChartRef.Kind, "namespace", template.Spec.Helm.ChartRef.Namespace, "name", template.Spec.Helm.ChartRef.Name)
return ctrl.Result{}, err
}
} else {
if template.Spec.Helm.ChartName == "" {
err = fmt.Errorf("neither chartName nor chartRef is set")
l.Error(err, "invalid helm chart reference")
return ctrl.Result{}, err
}
l.Info("Reconciling helm-controller objects ")
hcChart, err = r.reconcileHelmChart(ctx, template)
if err != nil {
l.Error(err, "Failed to reconcile HelmChart")
return ctrl.Result{}, err
}
}
if hcChart == nil {
// TODO: add externally referenced source verification
err := fmt.Errorf("HelmChart is nil")
l.Error(err, "could not get the helm chart")
return ctrl.Result{}, err
}

template.Status.ChartRef = &v2.CrossNamespaceSourceReference{
Kind: hcChart.Kind,
Kind: sourcev1.HelmChartKind,
Name: hcChart.Name,
Namespace: hcChart.Namespace,
}
Expand All @@ -96,8 +115,16 @@ func (r *TemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
return ctrl.Result{}, err
}

artifact := hcChart.Status.Artifact

if r.downloadHelmChartFunc == nil {
r.downloadHelmChartFunc = func(context.Context, *sourcev1.Artifact) (*chart.Chart, error) {
return helm.DownloadChartFromArtifact(ctx, artifact)
}
}

l.Info("Downloading Helm chart")
helmChart, err := helm.DownloadChartFromArtifact(ctx, hcChart.Status.Artifact)
helmChart, err := r.downloadHelmChartFunc(ctx, artifact)
if err != nil {
l.Error(err, "Failed to download Helm chart")
err = fmt.Errorf("failed to download chart: %s", err)
Expand Down Expand Up @@ -134,13 +161,17 @@ func (r *TemplateReconciler) parseChartMetadata(template *hmc.Template, chart *c
if chart.Metadata == nil {
return fmt.Errorf("chart metadata is empty")
}
templateType := chart.Metadata.Annotations[hmc.ChartAnnotationType]
switch hmc.TemplateType(templateType) {
case hmc.TemplateTypeDeployment, hmc.TemplateTypeProvider, hmc.TemplateTypeCore:
default:
return errNoProviderType
// the value in spec has higher priority
templateType := template.Spec.Type
if templateType == "" {
templateType = hmc.TemplateType(chart.Metadata.Annotations[hmc.ChartAnnotationType])
switch templateType {
case hmc.TemplateTypeDeployment, hmc.TemplateTypeProvider, hmc.TemplateTypeCore:
default:
return errNoProviderType
}
}
template.Status.Type = hmc.TemplateType(templateType)
template.Status.Type = templateType

// the value in spec has higher priority
if len(template.Spec.Providers.InfrastructureProviders) > 0 {
Expand Down Expand Up @@ -222,6 +253,21 @@ func (r *TemplateReconciler) reconcileHelmChart(ctx context.Context, template *h
return helmChart, nil
}

func (r *TemplateReconciler) getHelmChartFromChartRef(ctx context.Context, chartRef *helmcontrollerv2.CrossNamespaceSourceReference) (*sourcev1.HelmChart, error) {
if chartRef.Kind != sourcev1.HelmChartKind {
return nil, fmt.Errorf("invalid chartRef.Kind: %s. Only HelmChart kind is supported", chartRef.Kind)
}
helmChart := &sourcev1.HelmChart{}
err := r.Get(ctx, types.NamespacedName{
Namespace: chartRef.Namespace,
Name: chartRef.Name,
}, helmChart)
if err != nil {
return nil, err
}
return helmChart, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *TemplateReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Expand Down
71 changes: 66 additions & 5 deletions internal/controller/template_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import (
"context"

v2 "github.com/fluxcd/helm-controller/api/v2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"helm.sh/helm/v3/pkg/chart"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -31,6 +33,20 @@ import (
var _ = Describe("Template Controller", func() {
Context("When reconciling a resource", func() {
const resourceName = "test-resource"
const helmRepoNamespace = "default"
const helmRepoName = "test-helmrepo"
const helmChartName = "test-helmchart"
const helmChartURL = "http://source-controller.hmc-system.svc.cluster.local./helmchart/hmc-system/test-chart/0.1.0.tar.gz"

var fakeDownloadHelmChartFunc = func(context.Context, *sourcev1.Artifact) (*chart.Chart, error) {
return &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: "v2",
Version: "0.1.0",
Name: "test-chart",
},
}, nil
}

ctx := context.Background()

Expand All @@ -39,10 +55,53 @@ var _ = Describe("Template Controller", func() {
Namespace: "default", // TODO(user):Modify as needed
}
template := &hmcmirantiscomv1alpha1.Template{}
helmRepo := &sourcev1.HelmRepository{}
helmChart := &sourcev1.HelmChart{}

BeforeEach(func() {
By("creating helm repository")
err := k8sClient.Get(ctx, types.NamespacedName{Name: helmRepoName, Namespace: helmRepoNamespace}, helmRepo)
if err != nil && errors.IsNotFound(err) {
helmRepo = &sourcev1.HelmRepository{
ObjectMeta: metav1.ObjectMeta{
Name: helmRepoName,
Namespace: helmRepoNamespace,
},
Spec: sourcev1.HelmRepositorySpec{
URL: "oci://test/helmrepo",
},
}
Expect(k8sClient.Create(ctx, helmRepo)).To(Succeed())
}

By("creating helm chart")
err = k8sClient.Get(ctx, types.NamespacedName{Name: helmChartName, Namespace: helmRepoNamespace}, helmChart)
if err != nil && errors.IsNotFound(err) {
helmChart = &sourcev1.HelmChart{
ObjectMeta: metav1.ObjectMeta{
Name: helmChartName,
Namespace: helmRepoNamespace,
},
Spec: sourcev1.HelmChartSpec{
SourceRef: sourcev1.LocalHelmChartSourceReference{
Kind: sourcev1.HelmRepositoryKind,
Name: helmRepoName,
},
},
}
Expect(k8sClient.Create(ctx, helmChart)).To(Succeed())
}

By("updating HelmChart status with artifact URL")
helmChart.Status.URL = helmChartURL
helmChart.Status.Artifact = &sourcev1.Artifact{
URL: helmChartURL,
LastUpdateTime: metav1.Now(),
}
Expect(k8sClient.Status().Update(ctx, helmChart)).Should(Succeed())

By("creating the custom resource for the Kind Template")
err := k8sClient.Get(ctx, typeNamespacedName, template)
err = k8sClient.Get(ctx, typeNamespacedName, template)
if err != nil && errors.IsNotFound(err) {
resource := &hmcmirantiscomv1alpha1.Template{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -53,10 +112,11 @@ var _ = Describe("Template Controller", func() {
Helm: hmcmirantiscomv1alpha1.HelmSpec{
ChartRef: &v2.CrossNamespaceSourceReference{
Kind: "HelmChart",
Name: "ref-test",
Namespace: "default",
Name: helmChartName,
Namespace: helmRepoNamespace,
},
},
Type: hmcmirantiscomv1alpha1.TemplateTypeDeployment,
},
// TODO(user): Specify other spec details if needed.
}
Expand All @@ -76,8 +136,9 @@ var _ = Describe("Template Controller", func() {
It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
controllerReconciler := &TemplateReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
Client: k8sClient,
Scheme: k8sClient.Scheme(),
downloadHelmChartFunc: fakeDownloadHelmChartFunc,
}

_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
Expand Down

0 comments on commit 031b100

Please sign in to comment.