Implement render methods

Change-Id: I03b7aad33576eaa9ef7dfaabfb579ac90cc26a12
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
This commit is contained in:
Ruslan Aliev 2020-09-18 16:53:55 -05:00
parent 0bdbd690db
commit 0c736af2e8
33 changed files with 140 additions and 242 deletions

View File

@ -18,7 +18,7 @@ import (
"github.com/spf13/cobra"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/phase/render"
"opendev.org/airship/airshipctl/pkg/phase"
)
const (
@ -35,48 +35,48 @@ airshipctl phase render initinfra -l app=helm,service=tiller -k Deployment
// NewRenderCommand create a new command for document rendering
func NewRenderCommand(cfgFactory config.Factory) *cobra.Command {
renderSettings := &render.Settings{}
filterOptions := &phase.FilterOptions{}
renderCmd := &cobra.Command{
Use: "render PHASE_NAME",
Short: "Render phase documents from model",
Example: renderExample,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return renderSettings.Render(cfgFactory, args[0], cmd.OutOrStdout())
return filterOptions.Render(cfgFactory, args[0], cmd.OutOrStdout())
},
}
addRenderFlags(renderSettings, renderCmd)
addRenderFlags(filterOptions, renderCmd)
return renderCmd
}
// addRenderFlags adds flags for document render sub-command
func addRenderFlags(settings *render.Settings, cmd *cobra.Command) {
func addRenderFlags(filterOptions *phase.FilterOptions, cmd *cobra.Command) {
flags := cmd.Flags()
flags.StringVarP(
&settings.Label,
&filterOptions.Label,
"label",
"l",
"",
"filter documents by Labels")
flags.StringVarP(
&settings.Annotation,
&filterOptions.Annotation,
"annotation",
"a",
"",
"filter documents by Annotations")
flags.StringVarP(
&settings.APIVersion,
&filterOptions.APIVersion,
"apiversion",
"g",
"",
"filter documents by API version")
flags.StringVarP(
&settings.Kind,
&filterOptions.Kind,
"kind",
"k",
"",

View File

@ -17,44 +17,17 @@ package phase_test
import (
"testing"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipctl/cmd/phase"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/testutil"
)
func TestRender(t *testing.T) {
cfg, cleanupCfg := testutil.InitConfig(t)
defer cleanupCfg(t)
cfg.CurrentContext = "def_ephemeral"
cfg.Manifests["test"] = &config.Manifest{
TargetPath: "testdata",
PrimaryRepositoryName: "testRepo",
Repositories: map[string]*config.Repository{
"testRepo": {
URLString: "http://localhost",
},
},
}
ctx, err := cfg.GetContext("def_ephemeral")
require.NoError(t, err)
ctx.Manifest = "test"
settings := func() (*config.Config, error) {
return cfg, nil
}
tests := []*testutil.CmdTest{
{
Name: "render-with-help",
CmdLine: "-h",
Cmd: phase.NewRenderCommand(nil),
},
{
Name: "render-with-multiple-labels",
CmdLine: "initinfra -l app=helm,name=tiller",
Cmd: phase.NewRenderCommand(settings),
},
}
for _, tt := range tests {
testutil.RunTest(t, tt)

View File

@ -1,52 +0,0 @@
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
annotations:
airshipit.org/clustertype: ephemeral
creationTimestamp: null
labels:
app: helm
name: tiller
name: tiller-deploy
namespace: kube-system
spec:
replicas: 1
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: helm
name: tiller
spec:
automountServiceAccountToken: true
containers:
- env:
- name: TILLER_NAMESPACE
value: kube-system
- name: TILLER_HISTORY_MAX
value: "0"
image: gcr.io/kubernetes-helm/tiller:v2.12.3
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /liveness
port: 44135
initialDelaySeconds: 1
timeoutSeconds: 1
name: tiller
ports:
- containerPort: 44134
name: tiller
- containerPort: 44135
name: http
readinessProbe:
httpGet:
path: /readiness
port: 44135
initialDelaySeconds: 1
timeoutSeconds: 1
resources: {}
status: {}
...

View File

@ -1,2 +0,0 @@
resources:
- tiller.yaml

View File

@ -1,74 +0,0 @@
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
annotations:
airshipit.org/clustertype: ephemeral
creationTimestamp: null
labels:
app: helm
name: tiller
name: tiller-deploy
namespace: kube-system
spec:
replicas: 1
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: helm
name: tiller
spec:
automountServiceAccountToken: true
containers:
- env:
- name: TILLER_NAMESPACE
value: kube-system
- name: TILLER_HISTORY_MAX
value: "0"
image: gcr.io/kubernetes-helm/tiller:v2.12.3
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /liveness
port: 44135
initialDelaySeconds: 1
timeoutSeconds: 1
name: tiller
ports:
- containerPort: 44134
name: tiller
- containerPort: 44135
name: http
readinessProbe:
httpGet:
path: /readiness
port: 44135
initialDelaySeconds: 1
timeoutSeconds: 1
resources: {}
status: {}
---
apiVersion: v1
kind: Service
metadata:
annotations:
airshipit.org/clustertype: ephemeral
creationTimestamp: null
labels:
app: helm
name: tiller-deploy
namespace: kube-system
spec:
ports:
- name: tiller
port: 44134
targetPort: tiller
selector:
app: helm
name: tiller
type: ClusterIP
status:
loadBalancer: {}
...

1
go.sum
View File

@ -1507,6 +1507,7 @@ k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C
k8s.io/kube-state-metrics v1.7.2/go.mod h1:U2Y6DRi07sS85rmVPmBFlmv+2peBcL8IWGjM+IjYA/E=
k8s.io/kubectl v0.0.0-20191219154910-1528d4eea6dd h1:nZX5+wEqTu/EBIYjrZlFOA63z4+Zcy96lDkCZPU9a9c=
k8s.io/kubectl v0.0.0-20191219154910-1528d4eea6dd/go.mod h1:9ehGcuUGjXVZh0qbYSB0vvofQw2JQe6c6cO0k4wu/Oo=
k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8=
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
k8s.io/metrics v0.0.0-20191214191643-6b1944c9f765/go.mod h1:5V7rewilItwK0cz4nomU0b3XCcees2Ka5EBYWS1HBeM=
k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=

View File

@ -144,8 +144,10 @@ func (c *Executor) Validate() error {
}
// Render executor documents
func (c *Executor) Render(_ io.Writer, _ ifc.RenderOptions) error {
return errors.ErrNotImplemented{}
func (c *Executor) Render(w io.Writer, _ ifc.RenderOptions) error {
// will be implemented later
_, err := w.Write([]byte{})
return err
}
func handleError(ch chan<- events.Event, err error) {

View File

@ -182,6 +182,8 @@ func (c *ClusterctlExecutor) Validate() error {
}
// Render executor documents
func (c *ClusterctlExecutor) Render(_ io.Writer, _ ifc.RenderOptions) error {
return errors.ErrNotImplemented{}
func (c *ClusterctlExecutor) Render(w io.Writer, _ ifc.RenderOptions) error {
// will be implemented later
_, err := w.Write([]byte{})
return err
}

View File

@ -218,7 +218,6 @@ func TestExecutorRender(t *testing.T) {
sampleCfgDoc := executorDoc(t, "init")
bundle, err := document.NewBundleByPath("testdata/executor_init")
require.NoError(t, err)
expectedErr := airerrors.ErrNotImplemented{}
executor, err := cctlclient.NewExecutor(
ifc.ExecutorConfig{
@ -229,7 +228,7 @@ func TestExecutorRender(t *testing.T) {
require.NoError(t, err)
actualOut := &bytes.Buffer{}
actualErr := executor.Render(actualOut, ifc.RenderOptions{})
assert.Equal(t, expectedErr, actualErr)
assert.Equal(t, nil, actualErr)
}
func makeDefaultHelper(t *testing.T) ifc.Helper {

View File

@ -93,7 +93,7 @@ func (a *Applier) ApplyBundle(bundle document.Bundle, ao ApplyOptions) {
func (a *Applier) getInfos(bundleName string, bundle document.Bundle) ([]*resource.Info, error) {
if bundle == nil {
return nil, ErrApplyNilBundle{}
return nil, ErrNilBundle{}
}
selector := document.
NewSelector().

View File

@ -94,7 +94,7 @@ func TestApplierRun(t *testing.T) {
},
{
name: "bundle failure",
expectedString: "Cannot apply nil bundle",
expectedString: "nil bundle provided",
expectErr: true,
},
{

View File

@ -28,10 +28,10 @@ func (e ErrApply) Error() string {
return fmt.Sprintf("Applying of resources to kubernetes cluster has failed, errors are:\n%v", e.errors)
}
// ErrApplyNilBundle returned when nil bundle is passed to ApplyBundle function
type ErrApplyNilBundle struct {
// ErrNilBundle returned when bundle is nil
type ErrNilBundle struct {
}
func (e ErrApplyNilBundle) Error() string {
return "Cannot apply nil bundle"
func (e ErrNilBundle) Error() string {
return "nil bundle provided"
}

View File

@ -82,6 +82,9 @@ func NewExecutor(opts ExecutorOptions) (*Executor, error) {
if err != nil {
return nil, err
}
if opts.ExecutorBundle == nil {
return nil, ErrNilBundle{}
}
return &Executor{
Options: opts,
apiObject: apiObj,
@ -116,9 +119,6 @@ func (e *Executor) prepareApplier(ch chan events.Event) (*Applier, document.Bund
if err != nil {
return nil, nil, err
}
if e.Options.ExecutorBundle == nil {
return nil, nil, ErrApplyNilBundle{}
}
log.Debug("Filtering out documents that shouldn't be applied to kubernetes from document bundle")
bundle, err := e.Options.ExecutorBundle.SelectBundle(document.NewDeployToK8sSelector())
if err != nil {
@ -139,6 +139,10 @@ func (e *Executor) Validate() error {
}
// Render document set
func (e *Executor) Render(w io.Writer, _ ifc.RenderOptions) error {
return e.Options.ExecutorBundle.Write(w)
func (e *Executor) Render(w io.Writer, o ifc.RenderOptions) error {
bundle, err := e.Options.ExecutorBundle.SelectBundle(o.FilterSelector)
if err != nil {
return err
}
return bundle.Write(w)
}

View File

@ -167,7 +167,7 @@ func TestExecutorRun(t *testing.T) {
{
name: "Nil bundle provided",
execDoc: toKubernetesApply(t, ValidExecutorDoc),
containsErr: "Cannot apply nil bundle",
containsErr: "nil bundle provided",
kubeconf: testKubeconfig(testValidKubeconfig),
helper: makeDefaultHelper(t),
bundleFunc: func(t *testing.T) document.Bundle {
@ -185,17 +185,22 @@ func TestExecutorRun(t *testing.T) {
ExecutorBundle: tt.bundleFunc(t),
Kubeconfig: tt.kubeconf,
})
require.NoError(t, err)
require.NotNil(t, exec)
ch := make(chan events.Event)
go exec.Run(ch, ifc.RunOptions{})
processor := events.NewDefaultProcessor(utils.Streams())
err = processor.Process(ch)
if tt.containsErr != "" {
if tt.name == "Nil bundle provided" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.containsErr)
} else {
assert.NoError(t, err)
require.NoError(t, err)
require.NotNil(t, exec)
ch := make(chan events.Event)
go exec.Run(ch, ifc.RunOptions{})
processor := events.NewDefaultProcessor(utils.Streams())
err = processor.Process(ch)
if tt.containsErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.containsErr)
} else {
assert.NoError(t, err)
}
}
})
}

View File

@ -15,6 +15,7 @@
package phase
import (
"io"
"path/filepath"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -131,6 +132,16 @@ func (p *phase) Validate() error {
return nil
}
// Render executor documents
func (p *phase) Render(w io.Writer, options ifc.RenderOptions) error {
executor, err := p.Executor()
if err != nil {
return err
}
return executor.Render(w, options)
}
// DocumentRoot root that holds all the documents associated with the phase
func (p *phase) DocumentRoot() string {
if p.apiObj.Config.DocumentEntryPoint == "" {

View File

@ -177,11 +177,11 @@ var _ ifc.Executor = fakeExecutor{}
type fakeExecutor struct {
}
func (e fakeExecutor) Render(w io.Writer, ro ifc.RenderOptions) error {
func (e fakeExecutor) Render(_ io.Writer, _ ifc.RenderOptions) error {
return nil
}
func (e fakeExecutor) Run(ch chan events.Event, ro ifc.RunOptions) {
func (e fakeExecutor) Run(ch chan events.Event, _ ifc.RunOptions) {
defer close(ch)
}

View File

@ -40,8 +40,10 @@ type RunOptions struct {
Timeout time.Duration
}
// RenderOptions is empty for now, but may hold things like format in future
type RenderOptions struct{}
// RenderOptions holds options for render method
type RenderOptions struct {
FilterSelector document.Selector
}
// WaitOptions holds only timeout now, but may be extended in the future
type WaitOptions struct {

View File

@ -15,18 +15,20 @@
package ifc
import (
"io"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/cluster/clustermap"
)
// Phase provides a way to interact with a phase
// TODO add render method
type Phase interface {
Validate() error
Run(RunOptions) error
DocumentRoot() string
Details() (string, error)
Executor() (Executor, error)
Render(io.Writer, RenderOptions) error
}
// ID uniquely identifies the phase

View File

@ -12,7 +12,7 @@
limitations under the License.
*/
package render
package phase
import (
"io"
@ -20,26 +20,40 @@ import (
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
)
// FilterOptions holds filters for selector
type FilterOptions struct {
// Label filters documents by label string
Label string
// Annotation filters documents by annotation string
Annotation string
// APIVersion filters documents by API group and version
APIVersion string
// Kind filters documents by document kind
Kind string
}
// Render prints out filtered documents
func (s *Settings) Render(cfgFactory config.Factory, phaseName string, out io.Writer) error {
func (fo *FilterOptions) Render(cfgFactory config.Factory, phaseName string, out io.Writer) error {
cfg, err := cfgFactory()
if err != nil {
return err
}
path, err := cfg.CurrentContextEntryPoint(phaseName)
helper, err := NewHelper(cfg)
if err != nil {
return err
}
docBundle, err := document.NewBundleByPath(path)
client := NewClient(helper)
phase, err := client.PhaseByID(ifc.ID{Name: phaseName})
if err != nil {
return err
}
groupVersion := strings.Split(s.APIVersion, "/")
groupVersion := strings.Split(fo.APIVersion, "/")
group := ""
version := groupVersion[0]
if len(groupVersion) > 1 {
@ -47,11 +61,7 @@ func (s *Settings) Render(cfgFactory config.Factory, phaseName string, out io.Wr
version = strings.Join(groupVersion[1:], "/")
}
sel := document.NewSelector().ByLabel(s.Label).ByAnnotation(s.Annotation).ByGvk(group, version, s.Kind)
filteredBundle, err := docBundle.SelectBundle(sel)
if err != nil {
return err
}
sel := document.NewSelector().ByLabel(fo.Label).ByAnnotation(fo.Annotation).ByGvk(group, version, fo.Kind)
return filteredBundle.Write(out)
return phase.Render(out, ifc.RenderOptions{FilterSelector: sel})
}

View File

@ -1,27 +0,0 @@
/*
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 render
// Settings for document rendering
type Settings struct {
// Label filters documents by label string
Label string
// Annotation filters documents by annotation string
Annotation string
// APIVersion filters documents by API group and version
APIVersion string
// Kind filters documents by document kind
Kind string
}

View File

@ -12,7 +12,7 @@
limitations under the License.
*/
package render_test
package phase_test
import (
"bytes"
@ -25,7 +25,7 @@ import (
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/phase/render"
"opendev.org/airship/airshipctl/pkg/phase"
"opendev.org/airship/airshipctl/testutil"
)
@ -34,22 +34,23 @@ func TestRender(t *testing.T) {
dummyManifest := rs.Manifests["dummy_manifest"]
dummyManifest.TargetPath = "testdata"
dummyManifest.SubPath = ""
dummyManifest.MetadataPath = "metadata.yaml"
fixturePath := "phase"
tests := []struct {
name string
settings *render.Settings
settings *phase.FilterOptions
expResFile string
expErr error
}{
{
name: "No Filters",
settings: &render.Settings{},
settings: &phase.FilterOptions{},
expResFile: "noFilter.yaml",
expErr: nil,
},
{
name: "All Filters",
settings: &render.Settings{
settings: &phase.FilterOptions{
Label: "airshipit.org/deploy-k8s=false",
Annotation: "airshipit.org/clustertype=ephemeral",
APIVersion: "metal3.io/v1alpha1",
@ -60,7 +61,7 @@ func TestRender(t *testing.T) {
},
{
name: "Multiple Labels",
settings: &render.Settings{
settings: &phase.FilterOptions{
Label: "airshipit.org/deploy-k8s=false, airshipit.org/ephemeral-node=true",
},
expResFile: "multiLabels.yaml",
@ -68,7 +69,7 @@ func TestRender(t *testing.T) {
},
{
name: "Malformed Label",
settings: &render.Settings{
settings: &phase.FilterOptions{
Label: "app=(",
},
expResFile: "",

2
pkg/phase/testdata/metadata.yaml vendored Executable file
View File

@ -0,0 +1,2 @@
phase:
path: phases

11
pkg/phase/testdata/phases/cluster-map.yaml vendored Executable file
View File

@ -0,0 +1,11 @@
---
apiVersion: airshipit.org/v1alpha1
kind: ClusterMap
metadata:
labels:
airshipit.org/deploy-k8s: "false"
name: main-map
map:
target-cluster:
parent: ephemeral-cluster
ephemeral-cluster: {}

12
pkg/phase/testdata/phases/executors.yaml vendored Executable file
View File

@ -0,0 +1,12 @@
---
apiVersion: airshipit.org/v1alpha1
kind: KubernetesApply
metadata:
labels:
airshipit.org/deploy-k8s: "false"
name: kubernetes-apply
config:
waitOptions:
timeout: 2000
pruneOptions:
prune: false

View File

@ -0,0 +1,4 @@
resources:
- phases.yaml
- executors.yaml
- cluster-map.yaml

12
pkg/phase/testdata/phases/phases.yaml vendored Executable file
View File

@ -0,0 +1,12 @@
---
apiVersion: airshipit.org/v1alpha1
kind: Phase
metadata:
name: phase
clusterName: ephemeral-cluster
config:
executorRef:
apiVersion: airshipit.org/v1alpha1
kind: KubernetesApply
name: kubernetes-apply
documentEntryPoint: ephemeral/phase

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
set -e
set -xe
# The root of the manifest structure to be validated.
# This corresponds to the targetPath in an airshipctl config
@ -109,7 +109,7 @@ for cluster in ephemeral target; do
# removed since it was choking in certain cases and got to be more trouble than was worth.
# This should be removed once we have a phase map that is smarter.
# In the meantime, as new phases are added, please add them here as well.
phases="bootstrap initinfra controlplane baremetalhost workers workload tenant"
phases="initinfra-ephemeral controlplane-ephemeral initinfra-target workers-target"
for phase in $phases; do
# Guard against bootstrap or initinfra being missing, which could be the case for some configs