Skip to content

Commit

Permalink
Merge pull request #113 from eromanova/templates-validation
Browse files Browse the repository at this point in the history
Validate Templates with external source
  • Loading branch information
Kshatrix authored Jul 29, 2024
2 parents cc966c5 + 865f2b7 commit 4cca4fb
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 23 deletions.
27 changes: 26 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ tidy:
go mod tidy

.PHONY: test
test: generate-all fmt vet envtest tidy ## Run tests.
test: generate-all fmt vet envtest tidy external-crd ## 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 Expand Up @@ -318,6 +318,16 @@ LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
mkdir -p $(LOCALBIN)

EXTERNAL_CRD_DIR ?= $(LOCALBIN)/crd
$(EXTERNAL_CRD_DIR): $(LOCALBIN)
mkdir -p $(EXTERNAL_CRD_DIR)

FLUX_SOURCE_VERSION ?= $(shell go mod edit -json | jq -r '.Require[] | select(.Path == "github.com/fluxcd/source-controller/api") | .Version')
FLUX_SOURCE_REPO_CRD ?= $(EXTERNAL_CRD_DIR)/source-helmrepositories-$(FLUX_SOURCE_VERSION).yaml
FLUX_SOURCE_CHART_CRD ?= $(EXTERNAL_CRD_DIR)/source-helmchart-$(FLUX_SOURCE_VERSION).yaml
FLUX_HELM_VERSION ?= $(shell go mod edit -json | jq -r '.Require[] | select(.Path == "github.com/fluxcd/helm-controller/api") | .Version')
FLUX_HELM_CRD ?= $(EXTERNAL_CRD_DIR)/helm-$(FLUX_HELM_VERSION).yaml

## Tool Binaries
KUBECTL ?= kubectl
KUSTOMIZE ?= $(LOCALBIN)/kustomize-$(KUSTOMIZE_VERSION)
Expand Down Expand Up @@ -377,6 +387,21 @@ helmify: $(HELMIFY) ## Download helmify locally if necessary.
$(HELMIFY): | $(LOCALBIN)
$(call go-install-tool,$(HELMIFY),github.com/arttor/helmify/cmd/helmify,${HELMIFY_VERSION})

$(FLUX_HELM_CRD): $(EXTERNAL_CRD_DIR)
rm -f $(FLUX_HELM_CRD)
curl -s https://raw.githubusercontent.com/fluxcd/helm-controller/$(FLUX_HELM_VERSION)/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml > $(FLUX_HELM_CRD)

$(FLUX_SOURCE_CHART_CRD): $(EXTERNAL_CRD_DIR)
rm -f $(FLUX_SOURCE_CHART_CRD)
curl -s https://raw.githubusercontent.com/fluxcd/source-controller/$(FLUX_SOURCE_VERSION)/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml > $(FLUX_SOURCE_CHART_CRD)

$(FLUX_SOURCE_REPO_CRD): $(EXTERNAL_CRD_DIR)
rm -f $(FLUX_SOURCE_REPO_CRD)
curl -s https://raw.githubusercontent.com/fluxcd/source-controller/$(FLUX_SOURCE_VERSION)/config/crd/bases/source.toolkit.fluxcd.io_helmrepositories.yaml > $(FLUX_SOURCE_REPO_CRD)

.PHONY: external-crd
external-crd: $(FLUX_HELM_CRD) $(FLUX_SOURCE_CHART_CRD) $(FLUX_SOURCE_REPO_CRD)

.PHONY: kind
kind: $(KIND) ## Download kind locally if necessary.
$(KIND): | $(LOCALBIN)
Expand Down
3 changes: 2 additions & 1 deletion internal/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ 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("..", "..", "bin", "crd")},
ErrorIfCRDPathMissing: true,

// The BinaryAssetsDirectory is only required if you want to run the tests directly
Expand Down
76 changes: 60 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,14 @@ 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 = helm.DownloadChartFromArtifact
}

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 +159,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 +251,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 4cca4fb

Please sign in to comment.