diff --git a/cmd/phase/errors.go b/cmd/phase/errors.go new file mode 100644 index 000000000..0e9dd3cc0 --- /dev/null +++ b/cmd/phase/errors.go @@ -0,0 +1,26 @@ +/* + 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 + + https://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 phase + +import "fmt" + +// ErrRenderTooManyArgs returned when more than 1 argument is provided as argument to render command +type ErrRenderTooManyArgs struct { + Count int +} + +func (e *ErrRenderTooManyArgs) Error() string { + return fmt.Sprintf("accepts 1 or less arg(s), received %d", e.Count) +} diff --git a/cmd/phase/render.go b/cmd/phase/render.go index ad7451753..e0a28d86f 100644 --- a/cmd/phase/render.go +++ b/cmd/phase/render.go @@ -27,9 +27,15 @@ const ( # "service=tiller" airshipctl phase render initinfra -l app=helm,service=tiller -# Get all documents containing labels "app=helm" and "service=tiller" +# Get all phase documents containing labels "app=helm" and "service=tiller" # and kind 'Deployment' airshipctl phase render initinfra -l app=helm,service=tiller -k Deployment + +# Get all documents from config bundle +airshipctl phase render --source config + +# Get all documents executor rendered documents for a phase +airshipctl phase render initinfra --source executor ` ) @@ -40,9 +46,9 @@ func NewRenderCommand(cfgFactory config.Factory) *cobra.Command { Use: "render PHASE_NAME", Short: "Render phase documents from model", Example: renderExample, - Args: cobra.ExactArgs(1), + Args: RenderArgs(filterOptions), RunE: func(cmd *cobra.Command, args []string) error { - return filterOptions.RunE(cfgFactory, args[0], cmd.OutOrStdout()) + return filterOptions.RunE(cfgFactory, cmd.OutOrStdout()) }, } @@ -81,12 +87,27 @@ func addRenderFlags(filterOptions *phase.RenderCommand, cmd *cobra.Command) { "k", "", "filter documents by Kinds") - flags.BoolVarP( - &filterOptions.Executor, - "executor", - "e", - false, - "if set to true rendering will be performed by executor "+ - "otherwise phase entrypoint will be rendered by kustomize, if entrypoint is not specified "+ - "error will be returned") + + flags.StringVarP( + &filterOptions.Source, + "source", + "s", + phase.RenderSourcePhase, + "phase: phase entrypoint will be rendered by kustomize, if entrypoint is not specified "+ + "error will be returned\n"+ + "executor: rendering will be performed by executor if the phase\n"+ + "config: this will render bundle containing phase and executor documents") +} + +// RenderArgs returns an error if there are not exactly n args. +func RenderArgs(opts *phase.RenderCommand) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) > 1 { + return &ErrRenderTooManyArgs{Count: len(args)} + } + if len(args) == 1 { + opts.PhaseID.Name = args[0] + } + return nil + } } diff --git a/cmd/phase/render_test.go b/cmd/phase/render_test.go index d3bae377d..c4382d697 100644 --- a/cmd/phase/render_test.go +++ b/cmd/phase/render_test.go @@ -17,7 +17,10 @@ package phase_test import ( "testing" + "github.com/stretchr/testify/assert" + "opendev.org/airship/airshipctl/cmd/phase" + pkgphase "opendev.org/airship/airshipctl/pkg/phase" "opendev.org/airship/airshipctl/testutil" ) @@ -33,3 +36,32 @@ func TestRender(t *testing.T) { testutil.RunTest(t, tt) } } + +func TestRenderArgs(t *testing.T) { + tests := []struct { + name string + args []string + expectedErr error + }{ + { + name: "error 2 args", + args: []string{"my-phase", "accidental"}, + expectedErr: &phase.ErrRenderTooManyArgs{Count: 2}, + }, + { + name: "success", + args: []string{"my-phase"}, + }, + { + name: "success no args", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + err := phase.RenderArgs(&pkgphase.RenderCommand{})(phase.NewRenderCommand(nil), tt.args) + assert.Equal(t, tt.expectedErr, err) + }) + } +} diff --git a/cmd/phase/testdata/TestRenderGoldenOutput/render-with-help.golden b/cmd/phase/testdata/TestRenderGoldenOutput/render-with-help.golden index c7d0dec26..a16cefea5 100644 --- a/cmd/phase/testdata/TestRenderGoldenOutput/render-with-help.golden +++ b/cmd/phase/testdata/TestRenderGoldenOutput/render-with-help.golden @@ -9,15 +9,23 @@ Examples: # "service=tiller" airshipctl phase render initinfra -l app=helm,service=tiller -# Get all documents containing labels "app=helm" and "service=tiller" +# Get all phase documents containing labels "app=helm" and "service=tiller" # and kind 'Deployment' airshipctl phase render initinfra -l app=helm,service=tiller -k Deployment +# Get all documents from config bundle +airshipctl phase render --source config + +# Get all documents executor rendered documents for a phase +airshipctl phase render initinfra --source executor + Flags: -a, --annotation string filter documents by Annotations -g, --apiversion string filter documents by API version - -e, --executor if set to true rendering will be performed by executor otherwise phase entrypoint will be rendered by kustomize, if entrypoint is not specified error will be returned -h, --help help for render -k, --kind string filter documents by Kinds -l, --label string filter documents by Labels + -s, --source string phase: phase entrypoint will be rendered by kustomize, if entrypoint is not specified error will be returned + executor: rendering will be performed by executor if the phase + config: this will render bundle containing phase and executor documents (default "phase") diff --git a/docs/source/cli/airshipctl_phase_render.md b/docs/source/cli/airshipctl_phase_render.md index a2afb8867..c1c09cd6e 100644 --- a/docs/source/cli/airshipctl_phase_render.md +++ b/docs/source/cli/airshipctl_phase_render.md @@ -18,10 +18,16 @@ airshipctl phase render PHASE_NAME [flags] # "service=tiller" airshipctl phase render initinfra -l app=helm,service=tiller -# Get all documents containing labels "app=helm" and "service=tiller" +# Get all phase documents containing labels "app=helm" and "service=tiller" # and kind 'Deployment' airshipctl phase render initinfra -l app=helm,service=tiller -k Deployment +# Get all documents from config bundle +airshipctl phase render --source config + +# Get all documents executor rendered documents for a phase +airshipctl phase render initinfra --source executor + ``` ### Options @@ -29,10 +35,12 @@ airshipctl phase render initinfra -l app=helm,service=tiller -k Deployment ``` -a, --annotation string filter documents by Annotations -g, --apiversion string filter documents by API version - -e, --executor if set to true rendering will be performed by executor otherwise phase entrypoint will be rendered by kustomize, if entrypoint is not specified error will be returned -h, --help help for render -k, --kind string filter documents by Kinds -l, --label string filter documents by Labels + -s, --source string phase: phase entrypoint will be rendered by kustomize, if entrypoint is not specified error will be returned + executor: rendering will be performed by executor if the phase + config: this will render bundle containing phase and executor documents (default "phase") ``` ### Options inherited from parent commands diff --git a/docs/source/providers/cluster_api_docker.md b/docs/source/providers/cluster_api_docker.md index 78a6482a7..1759ef924 100755 --- a/docs/source/providers/cluster_api_docker.md +++ b/docs/source/providers/cluster_api_docker.md @@ -533,7 +533,7 @@ kube-system kube-scheduler-dtc-dtc-control-plane-p4fsx 1/1 Runn The command shown below can be used to delete the control plane, worker nodes and all associated cluster resources -$ airshipctl phase render controlplane -k Cluster | kubectl delete -f - +$ airshipctl phase render --phase-name controlplane -k Cluster | kubectl delete -f - ``` cluster.cluster.x-k8s.io "dtc" deleted diff --git a/docs/source/providers/cluster_api_gcp.md b/docs/source/providers/cluster_api_gcp.md index 0903d511c..125827445 100755 --- a/docs/source/providers/cluster_api_gcp.md +++ b/docs/source/providers/cluster_api_gcp.md @@ -458,7 +458,7 @@ If you would like to delete the cluster run the below commands. This will delete the control plane, workers, machine health check and all other resources associated with the cluster on gcp. -$ airshipctl phase render controlplane -k Cluster +$ airshipctl phase render --phase-name controlplane -k Cluster ``` --- @@ -483,7 +483,7 @@ spec: ... ``` -$ airshipctl phase render controlplane -k Cluster | kubectl delete -f - +$ airshipctl phase render --phase-name controlplane -k Cluster | kubectl delete -f - ``` cluster.cluster.x-k8s.io "gtc" deleted diff --git a/pkg/phase/errors.go b/pkg/phase/errors.go index 7a888f14c..1789359f1 100644 --- a/pkg/phase/errors.go +++ b/pkg/phase/errors.go @@ -63,3 +63,22 @@ func (e ErrExecutorRefNotDefined) Error() string { e.PhaseName, e.PhaseNamespace) } + +// ErrUknownRenderSource returned when render command source doesn't match any known types +type ErrUknownRenderSource struct { + Source string +} + +func (e ErrUknownRenderSource) Error() string { + return fmt.Sprintf("wrong render source '%s' specified must be one of %s, %s, %s", + e.Source, RenderSourceConfig, RenderSourceExecutor, RenderSourceExecutor) +} + +// ErrRenderPhaseNameNotSpecified returned when render command is called with either phase or +// executor source and phase name is not specified +type ErrRenderPhaseNameNotSpecified struct{} + +func (e ErrRenderPhaseNameNotSpecified) Error() string { + return fmt.Sprintf("must specify phase name when using '%s' or '%s' as source", + RenderSourceExecutor, RenderSourcePhase) +} diff --git a/pkg/phase/render.go b/pkg/phase/render.go index bdcf37f9b..9b42edbcb 100644 --- a/pkg/phase/render.go +++ b/pkg/phase/render.go @@ -23,6 +23,18 @@ import ( "opendev.org/airship/airshipctl/pkg/phase/ifc" ) +const ( + // RenderSourceConfig will render a bundle that comes from site metadata file + // and contains phase and executor docs + RenderSourceConfig = "config" + + // RenderSourceExecutor indicates that rendering will be delegated to phase executor + RenderSourceExecutor = "executor" + + // RenderSourcePhase the source will use kustomize root at phase entry point + RenderSourcePhase = "phase" +) + // RenderCommand holds filters for selector type RenderCommand struct { // Label filters documents by label string @@ -33,12 +45,19 @@ type RenderCommand struct { APIVersion string // Kind filters documents by document kind Kind string - // Executor identifies if executor should perform rendering - Executor bool + // Source identifies source of the bundle, these can be [phase|config|executor] + // phase the source will use kustomize root at phase entry point + // config will render a bundle that comes from site metadata file, and contains phase and executor docs + // executor means that rendering will be delegated to phase executor + Source string + PhaseID ifc.ID } // RunE prints out filtered documents -func (fo *RenderCommand) RunE(cfgFactory config.Factory, phaseName string, out io.Writer) error { +func (fo *RenderCommand) RunE(cfgFactory config.Factory, out io.Writer) error { + if err := fo.Validate(); err != nil { + return err + } cfg, err := cfgFactory() if err != nil { return err @@ -49,8 +68,12 @@ func (fo *RenderCommand) RunE(cfgFactory config.Factory, phaseName string, out i return err } + if fo.Source == RenderSourceConfig { + return renderConfigBundle(out, helper) + } + client := NewClient(helper) - phase, err := client.PhaseByID(ifc.ID{Name: phaseName}) + phase, err := client.PhaseByID(fo.PhaseID) if err != nil { return err } @@ -64,5 +87,32 @@ func (fo *RenderCommand) RunE(cfgFactory config.Factory, phaseName string, out i } sel := document.NewSelector().ByLabel(fo.Label).ByAnnotation(fo.Annotation).ByGvk(group, version, fo.Kind) - return phase.Render(out, fo.Executor, ifc.RenderOptions{FilterSelector: sel}) + var executorRender bool + if fo.Source == RenderSourceExecutor { + executorRender = true + } + return phase.Render(out, executorRender, ifc.RenderOptions{FilterSelector: sel}) +} + +func renderConfigBundle(out io.Writer, h ifc.Helper) error { + bundle, err := document.NewBundleByPath(h.PhaseBundleRoot()) + if err != nil { + return err + } + return bundle.Write(out) +} + +// Validate checks if command options are valid +func (fo *RenderCommand) Validate() (err error) { + switch fo.Source { + case RenderSourceConfig: + // do nothing, source config doesnt need any parameters + case RenderSourceExecutor, RenderSourcePhase: + if fo.PhaseID.Name == "" { + err = ErrRenderPhaseNameNotSpecified{} + } + default: + err = ErrUknownRenderSource{Source: fo.Source} + } + return err } diff --git a/pkg/phase/render_test.go b/pkg/phase/render_test.go index 018c86adb..28395f1af 100644 --- a/pkg/phase/render_test.go +++ b/pkg/phase/render_test.go @@ -26,6 +26,7 @@ import ( "opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/phase" + "opendev.org/airship/airshipctl/pkg/phase/ifc" "opendev.org/airship/airshipctl/testutil" ) @@ -43,13 +44,19 @@ func TestRender(t *testing.T) { fixturePath := "phase" tests := []struct { name string - settings *phase.RenderCommand expResFile string expErr error + + settings *phase.RenderCommand }{ { - name: "No Filters", - settings: &phase.RenderCommand{}, + name: "No Filters", + settings: &phase.RenderCommand{ + Source: phase.RenderSourcePhase, + PhaseID: ifc.ID{ + Name: fixturePath, + }, + }, expResFile: "noFilter.yaml", expErr: nil, }, @@ -60,6 +67,10 @@ func TestRender(t *testing.T) { Annotation: "airshipit.org/clustertype=ephemeral", APIVersion: "metal3.io/v1alpha1", Kind: "BareMetalHost", + Source: phase.RenderSourcePhase, + PhaseID: ifc.ID{ + Name: fixturePath, + }, }, expResFile: "allFilters.yaml", expErr: nil, @@ -67,7 +78,11 @@ func TestRender(t *testing.T) { { name: "Multiple Labels", settings: &phase.RenderCommand{ - Label: "airshipit.org/deploy-k8s=false, airshipit.org/ephemeral-node=true", + Label: "airshipit.org/deploy-k8s=false, airshipit.org/ephemeral-node=true", + Source: phase.RenderSourcePhase, + PhaseID: ifc.ID{ + Name: fixturePath, + }, }, expResFile: "multiLabels.yaml", expErr: nil, @@ -75,19 +90,38 @@ func TestRender(t *testing.T) { { name: "Malformed Label", settings: &phase.RenderCommand{ - Label: "app=(", + Label: "app=(", + Source: phase.RenderSourcePhase, + PhaseID: ifc.ID{ + Name: fixturePath, + }, }, - expResFile: "", - expErr: fmt.Errorf("unable to parse requirement: found '(', expected: identifier"), + expErr: fmt.Errorf("unable to parse requirement: found '(', expected: identifier"), }, { name: "Malformed Label", settings: &phase.RenderCommand{ - Label: "app=(", - Executor: true, + Label: "app=(", + Source: phase.RenderSourceExecutor, + PhaseID: ifc.ID{ + Name: fixturePath, + }, }, - expResFile: "", - expErr: fmt.Errorf("unable to parse requirement: found '(', expected: identifier"), + expErr: fmt.Errorf("unable to parse requirement: found '(', expected: identifier"), + }, + { + name: "source doesn't exist", + settings: &phase.RenderCommand{ + Source: "unknown", + }, + expErr: phase.ErrUknownRenderSource{Source: "unknown"}, + }, + { + name: "phase name not specified", + settings: &phase.RenderCommand{ + Source: phase.RenderSourcePhase, + }, + expErr: phase.ErrRenderPhaseNameNotSpecified{}, }, } @@ -103,9 +137,31 @@ func TestRender(t *testing.T) { out := &bytes.Buffer{} err = tt.settings.RunE(func() (*config.Config, error) { return rs, nil - }, fixturePath, out) + }, out) assert.Equal(t, tt.expErr, err) assert.Equal(t, expectedOut, out.Bytes()) }) } } + +func TestRenderConfigBundle(t *testing.T) { + rs := testutil.DummyConfig() + dummyManifest := rs.Manifests["dummy_manifest"] + dummyManifest.TargetPath = "testdata" + dummyManifest.PhaseRepositoryName = config.DefaultTestPhaseRepo + dummyManifest.Repositories = map[string]*config.Repository{ + config.DefaultTestPhaseRepo: {}, + } + dummyManifest.MetadataPath = "metadata.yaml" + buf := bytes.NewBuffer([]byte{}) + settings := &phase.RenderCommand{ + Source: phase.RenderSourceConfig, + } + err := settings.RunE(func() (*config.Config, error) { + return rs, nil + }, buf) + assert.NoError(t, err) + // check that it contains phases and cluster map + assert.Contains(t, buf.String(), "kind: Phase") + assert.Contains(t, buf.String(), "kind: ClusterMap") +}