Add ability to render different document sets

Now we would be able to render phase bundle, executor bundle
and config bundle. Config bundle will contain documents such as
phases and executors.

Relates-To: #459
Closes: #459

Change-Id: Ia6b9196dfb3d8fb3264fef676c975ccc32883fee
This commit is contained in:
Kostiantyn Kalynovskyi 2021-02-02 21:11:30 +00:00
parent f0e80cfdc5
commit 6807547ab2
10 changed files with 255 additions and 35 deletions

26
cmd/phase/errors.go Normal file
View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)
})
}
}

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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")
}