Add `(create|delete|export) source chart` commands

The `create source chart` command supports all HelmChart.spec fields
except `.valuesFiles` and `ignoreMissingValuesFiles` as these are
assumingly rarely used fields and the CLI usually only supports
commonly used ones.

closes #4760

Co-authored-by: Stefan Prodan <stefan.prodan@gmail.com>
Signed-off-by: Max Jonas Werner <mail@makk.es>
pull/4773/head
Max Jonas Werner 8 months ago
parent 9325eac0d3
commit 18d5ec4ecd
No known key found for this signature in database
GPG Key ID: EB525E0F02B52140

@ -0,0 +1,217 @@
/*
Copyright 2024 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 main
import (
"context"
"fmt"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
)
var createSourceChartCmd = &cobra.Command{
Use: "chart [name]",
Short: "Create or update a HelmChart source",
Long: `The create source chart command generates a HelmChart resource and waits for the chart to be available.`,
Example: ` # Create a source for a chart residing in a HelmRepository
flux create source chart podinfo \
--source=HelmRepository/podinfo \
--chart=podinfo \
--chart-version=6.x
# Create a source for a chart residing in a Git repository
flux create source chart podinfo \
--source=GitRepository/podinfo \
--chart=./charts/podinfo
# Create a source for a chart residing in a S3 Bucket
flux create source chart podinfo \
--source=Bucket/podinfo \
--chart=./charts/podinfo
# Create a source for a chart from OCI and verify its signature
flux create source chart podinfo \
--source HelmRepository/podinfo \
--chart podinfo \
--chart-version=6.6.2 \
--verify-provider=cosign \
--verify-issuer=https://token.actions.githubusercontent.com \
--verify-subject=https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2`,
RunE: createSourceChartCmdRun,
}
type sourceChartFlags struct {
chart string
chartVersion string
source flags.LocalHelmChartSource
reconcileStrategy string
verifyProvider flags.SourceOCIVerifyProvider
verifySecretRef string
verifyOIDCIssuer string
verifySubject string
}
var sourceChartArgs sourceChartFlags
func init() {
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.chart, "chart", "", "Helm chart name or path")
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.chartVersion, "chart-version", "", "Helm chart version, accepts a semver range (ignored for charts from GitRepository sources)")
createSourceChartCmd.Flags().Var(&sourceChartArgs.source, "source", sourceChartArgs.source.Description())
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.reconcileStrategy, "reconcile-strategy", "ChartVersion", "the reconcile strategy for helm chart (accepted values: Revision and ChartRevision)")
createSourceChartCmd.Flags().Var(&sourceChartArgs.verifyProvider, "verify-provider", sourceOCIRepositoryArgs.verifyProvider.Description())
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifySecretRef, "verify-secret-ref", "", "the name of a secret to use for signature verification")
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifySubject, "verify-subject", "", "regular expression to use for the OIDC subject during signature verification")
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification")
createSourceCmd.AddCommand(createSourceChartCmd)
}
func createSourceChartCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]
if sourceChartArgs.source.Kind == "" || sourceChartArgs.source.Name == "" {
return fmt.Errorf("chart source is required")
}
if sourceChartArgs.chart == "" {
return fmt.Errorf("chart name or path is required")
}
logger.Generatef("generating HelmChart source")
sourceLabels, err := parseLabels()
if err != nil {
return err
}
helmChart := &sourcev1.HelmChart{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: *kubeconfigArgs.Namespace,
Labels: sourceLabels,
},
Spec: sourcev1.HelmChartSpec{
Chart: sourceChartArgs.chart,
Version: sourceChartArgs.chartVersion,
Interval: metav1.Duration{
Duration: createArgs.interval,
},
ReconcileStrategy: sourceChartArgs.reconcileStrategy,
SourceRef: sourcev1.LocalHelmChartSourceReference{
Kind: sourceChartArgs.source.Kind,
Name: sourceChartArgs.source.Name,
},
},
}
if provider := sourceChartArgs.verifyProvider.String(); provider != "" {
helmChart.Spec.Verify = &sourcev1.OCIRepositoryVerification{
Provider: provider,
}
if secretName := sourceChartArgs.verifySecretRef; secretName != "" {
helmChart.Spec.Verify.SecretRef = &meta.LocalObjectReference{
Name: secretName,
}
}
verifyIssuer := sourceChartArgs.verifyOIDCIssuer
verifySubject := sourceChartArgs.verifySubject
if verifyIssuer != "" || verifySubject != "" {
helmChart.Spec.Verify.MatchOIDCIdentity = []sourcev1.OIDCIdentityMatch{{
Issuer: verifyIssuer,
Subject: verifySubject,
}}
}
} else if sourceChartArgs.verifySecretRef != "" {
return fmt.Errorf("a verification provider must be specified when a secret is specified")
} else if sourceChartArgs.verifyOIDCIssuer != "" || sourceOCIRepositoryArgs.verifySubject != "" {
return fmt.Errorf("a verification provider must be specified when OIDC issuer/subject is specified")
}
if createArgs.export {
return printExport(exportHelmChart(helmChart))
}
logger.Actionf("applying HelmChart source")
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
namespacedName, err := upsertHelmChart(ctx, kubeClient, helmChart)
if err != nil {
return err
}
logger.Waitingf("waiting for HelmChart source reconciliation")
readyConditionFunc := isObjectReadyConditionFunc(kubeClient, namespacedName, helmChart)
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, readyConditionFunc); err != nil {
return err
}
logger.Successf("HelmChart source reconciliation completed")
if helmChart.Status.Artifact == nil {
return fmt.Errorf("HelmChart source reconciliation completed but no artifact was found")
}
logger.Successf("fetched revision: %s", helmChart.Status.Artifact.Revision)
return nil
}
func upsertHelmChart(ctx context.Context, kubeClient client.Client,
helmChart *sourcev1.HelmChart) (types.NamespacedName, error) {
namespacedName := types.NamespacedName{
Namespace: helmChart.GetNamespace(),
Name: helmChart.GetName(),
}
var existing sourcev1.HelmChart
err := kubeClient.Get(ctx, namespacedName, &existing)
if err != nil {
if errors.IsNotFound(err) {
if err := kubeClient.Create(ctx, helmChart); err != nil {
return namespacedName, err
} else {
logger.Successf("source created")
return namespacedName, nil
}
}
return namespacedName, err
}
existing.Labels = helmChart.Labels
existing.Spec = helmChart.Spec
if err := kubeClient.Update(ctx, &existing); err != nil {
return namespacedName, err
}
helmChart = &existing
logger.Successf("source updated")
return namespacedName, nil
}

@ -0,0 +1,91 @@
//go:build unit
// +build unit
/*
Copyright 2024 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 main
import "testing"
func TestCreateSourceChart(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setupSourceChart(t, tmpl)
tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "missing name",
args: "create source chart --export",
assert: assertError("name is required"),
},
{
name: "missing source reference",
args: "create source chart podinfo --export ",
assert: assertError("chart source is required"),
},
{
name: "missing chart name",
args: "create source chart podinfo --source helmrepository/podinfo --export",
assert: assertError("chart name or path is required"),
},
{
name: "unknown source kind",
args: "create source chart podinfo --source foobar/podinfo --export",
assert: assertError(`invalid argument "foobar/podinfo" for "--source" flag: source kind 'foobar' is not supported, must be one of: HelmRepository, GitRepository, Bucket`),
},
{
name: "basic chart",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --export",
assert: assertGoldenTemplateFile("testdata/create_source_chart/basic.yaml", tmpl),
},
{
name: "chart with basic signature verification",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider cosign --export",
assert: assertGoldenTemplateFile("testdata/create_source_chart/verify_basic.yaml", tmpl),
},
{
name: "unknown signature verification provider",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider foobar --export",
assert: assertError(`invalid argument "foobar" for "--verify-provider" flag: source OCI verify provider 'foobar' is not supported, must be one of: cosign`),
},
{
name: "chart with complete signature verification",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider cosign --verify-issuer foo --verify-subject bar --export",
assert: assertGoldenTemplateFile("testdata/create_source_chart/verify_complete.yaml", tmpl),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args + " -n " + tmpl["fluxns"],
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}
func setupSourceChart(t *testing.T, tmpl map[string]string) {
t.Helper()
testEnv.CreateObjectFile("./testdata/create_source_chart/setup-source.yaml", tmpl, t)
}

@ -0,0 +1,40 @@
/*
Copyright 2024 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 main
import (
"github.com/spf13/cobra"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
)
var deleteSourceChartCmd = &cobra.Command{
Use: "chart [name]",
Short: "Delete a HelmChart source",
Long: "The delete source chart command deletes the given HelmChart from the cluster.",
Example: ` # Delete a HelmChart
flux delete source chart podinfo`,
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)),
RunE: deleteCommand{
apiType: helmChartType,
object: universalAdapter{&sourcev1.HelmChart{}},
}.run,
}
func init() {
deleteSourceCmd.AddCommand(deleteSourceChartCmd)
}

@ -0,0 +1,67 @@
/*
Copyright 2024 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 main
import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
)
var exportSourceChartCmd = &cobra.Command{
Use: "chart [name]",
Short: "Export HelmChart sources in YAML format",
Long: withPreviewNote("The export source chart command exports one or all HelmChart sources in YAML format."),
Example: ` # Export all chart sources
flux export source chart --all > sources.yaml`,
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)),
RunE: exportCommand{
list: helmChartListAdapter{&sourcev1.HelmChartList{}},
object: helmChartAdapter{&sourcev1.HelmChart{}},
}.run,
}
func init() {
exportSourceCmd.AddCommand(exportSourceChartCmd)
}
func exportHelmChart(source *sourcev1.HelmChart) interface{} {
gvk := sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)
export := sourcev1.HelmChart{
TypeMeta: metav1.TypeMeta{
Kind: gvk.Kind,
APIVersion: gvk.GroupVersion().String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: source.Name,
Namespace: source.Namespace,
Labels: source.Labels,
Annotations: source.Annotations,
},
Spec: source.Spec,
}
return export
}
func (ex helmChartAdapter) export() interface{} {
return exportHelmChart(ex.HelmChart)
}
func (ex helmChartListAdapter) exportItem(i int) interface{} {
return exportHelmChart(&ex.HelmChartList.Items[i])
}

@ -58,6 +58,12 @@ func TestExport(t *testing.T) {
"testdata/export/git-repo.yaml", "testdata/export/git-repo.yaml",
tmpl, tmpl,
}, },
{
"source chart",
"export source chart flux-system",
"testdata/export/helm-chart.yaml",
tmpl,
},
{ {
"source helm", "source helm",
"export source helm flux-system", "export source helm flux-system",

@ -0,0 +1,14 @@
✚ generating HelmChart source
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmChart
metadata:
name: podinfo
namespace: {{ .fluxns }}
spec:
chart: podinfo
interval: 0s
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: podinfo

@ -0,0 +1,16 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: {{ .fluxns }}
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: podinfo
namespace: {{ .fluxns }}
spec:
interval: 1m0s
provider: generic
type: oci
url: oci://ghcr.io/stefanprodan/charts

@ -0,0 +1,16 @@
✚ generating HelmChart source
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmChart
metadata:
name: podinfo
namespace: {{ .fluxns }}
spec:
chart: podinfo
interval: 0s
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: podinfo
verify:
provider: cosign

@ -0,0 +1,19 @@
✚ generating HelmChart source
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmChart
metadata:
name: podinfo
namespace: {{ .fluxns }}
spec:
chart: podinfo
interval: 0s
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: podinfo
verify:
matchOIDCIdentity:
- issuer: foo
subject: bar
provider: cosign

@ -0,0 +1,14 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmChart
metadata:
name: flux-system
namespace: {{ .fluxns }}
spec:
chart: podinfo
interval: 1m0s
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: podinfo
version: '*'

@ -124,6 +124,20 @@ spec:
timeout: 1m0s timeout: 1m0s
url: https://stefanprodan.github.io/podinfo url: https://stefanprodan.github.io/podinfo
--- ---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmChart
metadata:
name: flux-system
namespace: {{ .fluxns }}
spec:
chart: podinfo
interval: 1m0s
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: podinfo
version: '*'
---
apiVersion: helm.toolkit.fluxcd.io/v2beta2 apiVersion: helm.toolkit.fluxcd.io/v2beta2
kind: HelmRelease kind: HelmRelease
metadata: metadata:

@ -0,0 +1,70 @@
/*
Copyright 2024 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 flags
import (
"fmt"
"strings"
"github.com/fluxcd/flux2/v2/internal/utils"
)
type LocalHelmChartSource struct {
Kind string
Name string
}
func (s *LocalHelmChartSource) String() string {
if s.Name == "" {
return ""
}
return fmt.Sprintf("%s/%s", s.Kind, s.Name)
}
func (s *LocalHelmChartSource) Set(str string) error {
if strings.TrimSpace(str) == "" {
return fmt.Errorf("no helm chart source given, please specify %s",
s.Description())
}
sourceKind, sourceName := utils.ParseObjectKindName(str)
if sourceKind == "" || sourceName == "" {
return fmt.Errorf("invalid helm chart source '%s', must be in format <kind>/<name>", str)
}
cleanSourceKind, ok := utils.ContainsEqualFoldItemString(supportedHelmChartSourceKinds, sourceKind)
if !ok {
return fmt.Errorf("source kind '%s' is not supported, must be one of: %s",
sourceKind, strings.Join(supportedHelmChartSourceKinds, ", "))
}
s.Kind = cleanSourceKind
s.Name = sourceName
return nil
}
func (s *LocalHelmChartSource) Type() string {
return "helmChartSource"
}
func (s *LocalHelmChartSource) Description() string {
return fmt.Sprintf(
"source that contains the chart in the format '<kind>/<name>', "+
"where kind must be one of: (%s)",
strings.Join(supportedHelmChartSourceKinds, ", "),
)
}
Loading…
Cancel
Save