-
Notifications
You must be signed in to change notification settings - Fork 616
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4773 from fluxcd/source-chart-cmds
Add `(create|delete|export) source chart` commands
- Loading branch information
Showing
12 changed files
with
584 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
Oops, something went wrong.