diff --git a/cmd/flux/build_kustomization.go b/cmd/flux/build_kustomization.go index e093ac963a..a2626b2f22 100644 --- a/cmd/flux/build_kustomization.go +++ b/cmd/flux/build_kustomization.go @@ -40,7 +40,10 @@ It is possible to specify a Flux kustomization file using --kustomization-file.` flux build kustomization my-app --path ./path/to/local/manifests # Build using a local flux kustomization file -flux build kustomization my-app --path ./path/to/local/manifests --kustomization-file ./path/to/local/my-app.yaml`, +flux build kustomization my-app --path ./path/to/local/manifests --kustomization-file ./path/to/local/my-app.yaml + +# Build in dry-run mode, i.e. do not connect to the cluster at all +flux build kustomization my-app --path ./path/to/local/manifests --kustomization-file ./path/to/local/my-app.yaml --dry-run`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: buildKsCmdRun, } @@ -48,6 +51,7 @@ flux build kustomization my-app --path ./path/to/local/manifests --kustomization type buildKsFlags struct { kustomizationFile string path string + dryRun bool } var buildKsArgs buildKsFlags @@ -55,10 +59,11 @@ var buildKsArgs buildKsFlags func init() { buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.") buildKsCmd.Flags().StringVar(&buildKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.") + buildKsCmd.Flags().BoolVar(&buildKsArgs.dryRun, "dry-run", false, "Dry run mode.") buildCmd.AddCommand(buildKsCmd) } -func buildKsCmdRun(cmd *cobra.Command, args []string) error { +func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) { if len(args) < 1 { return fmt.Errorf("%s name is required", kustomizationType.humanKind) } @@ -72,13 +77,22 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid resource path %q", buildKsArgs.path) } + if buildKsArgs.dryRun && buildKsArgs.kustomizationFile == "" { + return fmt.Errorf("dry-run mode requires a kustomization file") + } + if buildKsArgs.kustomizationFile != "" { if fs, err := os.Stat(buildKsArgs.kustomizationFile); os.IsNotExist(err) || fs.IsDir() { return fmt.Errorf("invalid kustomization file %q", buildKsArgs.kustomizationFile) } } - builder, err := build.NewBuilder(kubeconfigArgs, kubeclientOptions, name, buildKsArgs.path, build.WithTimeout(rootArgs.timeout), build.WithKustomizationFile(buildKsArgs.kustomizationFile)) + builder, err := build.NewBuilder(name, buildKsArgs.path, + build.WithClientConfig(kubeconfigArgs, kubeclientOptions), + build.WithTimeout(rootArgs.timeout), + build.WithKustomizationFile(buildKsArgs.kustomizationFile), + build.WithDryRun(buildKsArgs.dryRun), + ) if err != nil { return err } diff --git a/cmd/flux/build_kustomization_test.go b/cmd/flux/build_kustomization_test.go index aba1b16ca3..5d78a7cb94 100644 --- a/cmd/flux/build_kustomization_test.go +++ b/cmd/flux/build_kustomization_test.go @@ -142,6 +142,12 @@ spec: resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", assertFunc: "assertGoldenTemplateFile", }, + { + name: "build deployment and configmap with var substitution in dry-run mode", + args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/var-substitution --dry-run", + resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, } tmpl := map[string]string{ diff --git a/cmd/flux/diff_kustomization.go b/cmd/flux/diff_kustomization.go index baeda3631c..335717f656 100644 --- a/cmd/flux/diff_kustomization.go +++ b/cmd/flux/diff_kustomization.go @@ -77,12 +77,21 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { } } - var builder *build.Builder - var err error + var ( + builder *build.Builder + err error + ) if diffKsArgs.progressBar { - builder, err = build.NewBuilder(kubeconfigArgs, kubeclientOptions, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout), build.WithKustomizationFile(diffKsArgs.kustomizationFile), build.WithProgressBar()) + builder, err = build.NewBuilder(name, diffKsArgs.path, + build.WithClientConfig(kubeconfigArgs, kubeclientOptions), + build.WithTimeout(rootArgs.timeout), + build.WithKustomizationFile(diffKsArgs.kustomizationFile), + build.WithProgressBar()) } else { - builder, err = build.NewBuilder(kubeconfigArgs, kubeclientOptions, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout), build.WithKustomizationFile(diffKsArgs.kustomizationFile)) + builder, err = build.NewBuilder(name, diffKsArgs.path, + build.WithClientConfig(kubeconfigArgs, kubeclientOptions), + build.WithTimeout(rootArgs.timeout), + build.WithKustomizationFile(diffKsArgs.kustomizationFile)) } if err != nil { diff --git a/cmd/flux/diff_kustomization_test.go b/cmd/flux/diff_kustomization_test.go index 60e73a6ca6..3c7a3071c0 100644 --- a/cmd/flux/diff_kustomization_test.go +++ b/cmd/flux/diff_kustomization_test.go @@ -97,7 +97,7 @@ func TestDiffKustomization(t *testing.T) { "fluxns": allocateNamespace("flux-system"), } - b, _ := build.NewBuilder(kubeconfigArgs, kubeclientOptions, "podinfo", "") + b, _ := build.NewBuilder("podinfo", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions)) resourceManager, err := b.Manager() if err != nil { diff --git a/go.mod b/go.mod index a9139e112c..d3b716c170 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/fluxcd/flux2 go 1.18 -replace github.com/fluxcd/pkg/kustomize => github.com/souleb/pkg/kustomize v0.0.0-20221114164450-c3ad651f477b +replace github.com/fluxcd/pkg/kustomize => github.com/souleb/pkg/kustomize v0.0.0-20221115105652-bfcd8988ecc8 require ( github.com/Masterminds/semver/v3 v3.1.1 diff --git a/go.sum b/go.sum index 0db981b7fa..ed3fa64f8d 100644 --- a/go.sum +++ b/go.sum @@ -592,8 +592,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/souleb/pkg/kustomize v0.0.0-20221114164450-c3ad651f477b h1:JlaF96cEGlAxdW7NYi8ON398wR2xtrJkRUAAHFCTh0w= -github.com/souleb/pkg/kustomize v0.0.0-20221114164450-c3ad651f477b/go.mod h1:rXQcYjvqqS+9oCOA2J/w7KTnwNhdwDCeW4mE5zQRjN4= +github.com/souleb/pkg/kustomize v0.0.0-20221115105652-bfcd8988ecc8 h1:8ffW88dnCuwYL3zZRkFfxM4OCM3kfT4a2fzMH4HQDWY= +github.com/souleb/pkg/kustomize v0.0.0-20221115105652-bfcd8988ecc8/go.mod h1:rXQcYjvqqS+9oCOA2J/w7KTnwNhdwDCeW4mE5zQRjN4= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= diff --git a/internal/build/build.go b/internal/build/build.go index 8786be6bf8..f4c3fca96f 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -76,10 +76,13 @@ type Builder struct { kustomization *kustomizev1.Kustomization timeout time.Duration spinner *yacspin.Spinner + dryRun bool } +// BuilderOptionFunc is a function that configures a Builder type BuilderOptionFunc func(b *Builder) error +// WithKustomizationFile sets the kustomization file func WithKustomizationFile(file string) BuilderOptionFunc { return func(b *Builder) error { b.kustomizationFile = file @@ -87,6 +90,7 @@ func WithKustomizationFile(file string) BuilderOptionFunc { } } +// WithTimeout sets the timeout for the builder func WithTimeout(timeout time.Duration) BuilderOptionFunc { return func(b *Builder) error { b.timeout = timeout @@ -116,24 +120,47 @@ func WithProgressBar() BuilderOptionFunc { } } -// NewBuilder returns a new Builder -// to dp : create functional options -func NewBuilder(rcg *genericclioptions.ConfigFlags, clientOpts *runclient.Options, name, resources string, opts ...BuilderOptionFunc) (*Builder, error) { - kubeClient, err := utils.KubeClient(rcg, clientOpts) - if err != nil { - return nil, err +// WithClientConfig sets the client configuration +func WithClientConfig(rcg *genericclioptions.ConfigFlags, clientOpts *runclient.Options) BuilderOptionFunc { + return func(b *Builder) error { + kubeClient, err := utils.KubeClient(rcg, clientOpts) + if err != nil { + return err + } + + restMapper, err := rcg.ToRESTMapper() + if err != nil { + return err + } + b.client = kubeClient + b.restMapper = restMapper + b.namespace = *rcg.Namespace + return nil } +} - restMapper, err := rcg.ToRESTMapper() - if err != nil { - return nil, err +// WithDryRun sets the dry-run flag +func WithDryRun(dryRun bool) BuilderOptionFunc { + return func(b *Builder) error { + b.dryRun = dryRun + return nil } +} +// NewBuilder returns a new Builder +// It takes a kustomization name and a path to the resources +// It also takes a list of BuilderOptionFunc to configure the builder +// One of the options is WithClientConfig, that must be provided for the builder to work +// with the k8s cluster +// One other option is WithKustomizationFile, that must be provided for the builder to work +// with a local kustomization file. If the kustomization file is not provided, the builder +// will try to retrieve the kustomization object from the k8s cluster. +// WithDryRun sets the dry-run flag, and needs to be provided if the builder is used for +// a dry-run. This flag works in conjunction with WithKustomizationFile, because the +// kustomization object is not retrieved from the k8s cluster when the dry-run flag is set. +func NewBuilder(name, resources string, opts ...BuilderOptionFunc) (*Builder, error) { b := &Builder{ - client: kubeClient, - restMapper: restMapper, name: name, - namespace: *rcg.Namespace, resourcesPath: resources, } @@ -147,6 +174,14 @@ func NewBuilder(rcg *genericclioptions.ConfigFlags, clientOpts *runclient.Option b.timeout = defaultTimeout } + if b.dryRun && b.kustomizationFile == "" { + return nil, fmt.Errorf("kustomization file is required for dry-run") + } + + if !b.dryRun && b.client == nil { + return nil, fmt.Errorf("client is required for live run") + } + return b, nil } @@ -301,7 +336,7 @@ func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomizatio if err != nil { return nil, err } - outRes, err := kustomize.SubstituteVariables(ctx, b.client, unstructured.Unstructured{Object: data}, res, false) + outRes, err := kustomize.SubstituteVariables(ctx, b.client, unstructured.Unstructured{Object: data}, res, b.dryRun) if err != nil { return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err) }