Integrate cli-utils applier with phases.

This will also enable us to wait for kubernetes resources to reach
required state

Relates-To: #238

Change-Id: Ia2c9cebd94ca2ef5bfd9ed5830ae469e6aa6b167
This commit is contained in:
Kostiantyn Kalynovskyi 2020-06-16 16:39:35 -05:00
parent f5cf2d379a
commit e4adce2bcf
8 changed files with 115 additions and 100 deletions

View File

@ -17,10 +17,11 @@ limitations under the License.
package phase package phase
import ( import (
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"opendev.org/airship/airshipctl/pkg/environment" "opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/phase/apply" "opendev.org/airship/airshipctl/pkg/phase/apply"
) )
@ -35,9 +36,10 @@ airshipctl phase apply initinfra
) )
// NewApplyCommand creates a command to apply phase to k8s cluster. // NewApplyCommand creates a command to apply phase to k8s cluster.
func NewApplyCommand(rootSettings *environment.AirshipCTLSettings, factory client.Factory) *cobra.Command { func NewApplyCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Command {
i := apply.NewOptions(rootSettings) i := &apply.Options{
RootSettings: rootSettings,
}
applyCmd := &cobra.Command{ applyCmd := &cobra.Command{
Use: "apply PHASE_NAME", Use: "apply PHASE_NAME",
Short: "Apply phase to a cluster", Short: "Apply phase to a cluster",
@ -46,12 +48,7 @@ func NewApplyCommand(rootSettings *environment.AirshipCTLSettings, factory clien
Example: applyExample, Example: applyExample,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
i.PhaseName = args[0] i.PhaseName = args[0]
client, err := factory(rootSettings) i.Initialize()
if err != nil {
return err
}
i.Client = client
return i.Run() return i.Run()
}, },
} }
@ -73,4 +70,10 @@ func addApplyFlags(i *apply.Options, cmd *cobra.Command) {
false, false,
`if set to true, command will delete all kubernetes resources that are not`+ `if set to true, command will delete all kubernetes resources that are not`+
` defined in airship documents and have airshipit.org/deployed=apply label`) ` defined in airship documents and have airshipit.org/deployed=apply label`)
flags.DurationVar(
&i.WaitTimeout,
"wait-timeout",
time.Second*120,
`number of seconds to wait for resources to become ready, if set to 0 will not wait`)
} }

View File

@ -19,8 +19,6 @@ import (
"opendev.org/airship/airshipctl/cmd/phase" "opendev.org/airship/airshipctl/cmd/phase"
"opendev.org/airship/airshipctl/pkg/environment" "opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/k8s/client/fake"
"opendev.org/airship/airshipctl/testutil" "opendev.org/airship/airshipctl/testutil"
) )
@ -30,15 +28,12 @@ func TestNewApplyCommand(t *testing.T) {
KubeConfigPath: "../../testdata/k8s/kubeconfig.yaml", KubeConfigPath: "../../testdata/k8s/kubeconfig.yaml",
} }
fakeRootSettings.InitConfig() fakeRootSettings.InitConfig()
testClientFactory := func(_ *environment.AirshipCTLSettings) (client.Interface, error) {
return fake.NewClient(), nil
}
tests := []*testutil.CmdTest{ tests := []*testutil.CmdTest{
{ {
Name: "phase-apply-cmd-with-help", Name: "phase-apply-cmd-with-help",
CmdLine: "--help", CmdLine: "--help",
Cmd: phase.NewApplyCommand(fakeRootSettings, testClientFactory), Cmd: phase.NewApplyCommand(fakeRootSettings),
}, },
} }
for _, testcase := range tests { for _, testcase := range tests {

View File

@ -18,7 +18,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"opendev.org/airship/airshipctl/pkg/environment" "opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/log" "opendev.org/airship/airshipctl/pkg/log"
) )
@ -37,13 +36,12 @@ func NewPhaseCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Comman
Long: clusterLong[1:], Long: clusterLong[1:],
PersistentPreRun: func(cmd *cobra.Command, args []string) { PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.Init(rootSettings.Debug, cmd.OutOrStderr()) log.Init(rootSettings.Debug, cmd.OutOrStderr())
// Load or Initialize airship Config // Load or Initialize airship Config
rootSettings.InitConfig() rootSettings.InitConfig()
}, },
} }
phaseRootCmd.AddCommand(NewApplyCommand(rootSettings, client.DefaultClient)) phaseRootCmd.AddCommand(NewApplyCommand(rootSettings))
phaseRootCmd.AddCommand(NewRenderCommand(rootSettings)) phaseRootCmd.AddCommand(NewRenderCommand(rootSettings))
phaseRootCmd.AddCommand(NewPlanCommand(rootSettings)) phaseRootCmd.AddCommand(NewPlanCommand(rootSettings))

View File

@ -10,6 +10,7 @@ airshipctl phase apply initinfra
Flags: Flags:
--dry-run don't deliver documents to the cluster, simulate the changes instead --dry-run don't deliver documents to the cluster, simulate the changes instead
-h, --help help for apply -h, --help help for apply
--prune if set to true, command will delete all kubernetes resources that are not defined in airship documents and have airshipit.org/deployed=apply label --prune if set to true, command will delete all kubernetes resources that are not defined in airship documents and have airshipit.org/deployed=apply label
--wait-timeout duration number of seconds to wait for resources to become ready, if set to 0 will not wait (default 2m0s)

View File

@ -23,9 +23,10 @@ airshipctl phase apply initinfra
### Options ### Options
``` ```
--dry-run don't deliver documents to the cluster, simulate the changes instead --dry-run don't deliver documents to the cluster, simulate the changes instead
-h, --help help for apply -h, --help help for apply
--prune if set to true, command will delete all kubernetes resources that are not defined in airship documents and have airshipit.org/deployed=apply label --prune if set to true, command will delete all kubernetes resources that are not defined in airship documents and have airshipit.org/deployed=apply label
--wait-timeout duration number of seconds to wait for resources to become ready, if set to 0 will not wait (default 2m0s)
``` ```
### Options inherited from parent commands ### Options inherited from parent commands

View File

@ -15,66 +15,72 @@
package apply package apply
import ( import (
"fmt"
"time"
"opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/environment" "opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/k8s/client" "opendev.org/airship/airshipctl/pkg/events"
"opendev.org/airship/airshipctl/pkg/k8s/applier"
"opendev.org/airship/airshipctl/pkg/k8s/utils"
"opendev.org/airship/airshipctl/pkg/log"
) )
// Options is an abstraction used to apply the phase // Options is an abstraction used to apply the phase
type Options struct { type Options struct {
RootSettings *environment.AirshipCTLSettings RootSettings *environment.AirshipCTLSettings
Client client.Interface Applier *applier.Applier
Processor events.EventProcessor
DryRun bool WaitTimeout time.Duration
Prune bool DryRun bool
PhaseName string Prune bool
PhaseName string
} }
// NewOptions return instance of Options // Initialize Options with required field, such as Applier
func NewOptions(settings *environment.AirshipCTLSettings) *Options { func (o *Options) Initialize() {
// At this point AirshipCTLSettings may not be fully initialized f := utils.FactoryFromKubeConfigPath(o.RootSettings.KubeConfigPath)
applyOptions := &Options{RootSettings: settings} streams := utils.Streams()
return applyOptions o.Applier = applier.NewApplier(f, streams)
o.Processor = events.NewDefaultProcessor(streams)
} }
// Run apply subcommand logic // Run apply subcommand logic
func (applyOptions *Options) Run() error { func (o *Options) Run() error {
kctl := applyOptions.Client.Kubectl() ao := applier.ApplyOptions{
ao, err := kctl.ApplyOptions() DryRun: o.DryRun,
Prune: o.Prune,
WaitTimeout: o.WaitTimeout,
}
globalConf := o.RootSettings.Config
if err := globalConf.EnsureComplete(); err != nil {
return err
}
clusterName, err := globalConf.CurrentContextClusterName()
if err != nil { if err != nil {
return err return err
} }
clusterType, err := globalConf.CurrentContextClusterType()
ao.SetDryRun(applyOptions.DryRun)
// If prune is true, set selector for pruning
if applyOptions.Prune {
ao.SetPrune(document.ApplyPhaseSelector + applyOptions.PhaseName)
}
globalConf := applyOptions.RootSettings.Config
if err = globalConf.EnsureComplete(); err != nil {
return err
}
kustomizePath, err := globalConf.CurrentContextEntryPoint(applyOptions.PhaseName)
if err != nil { if err != nil {
return err return err
} }
ao.BundleName = fmt.Sprintf("%s-%s-%s", clusterName, clusterType, o.PhaseName)
kustomizePath, err := globalConf.CurrentContextEntryPoint(o.PhaseName)
if err != nil {
return err
}
log.Debugf("building bundle from kustomize path %s", kustomizePath)
b, err := document.NewBundleByPath(kustomizePath) b, err := document.NewBundleByPath(kustomizePath)
if err != nil { if err != nil {
return err return err
} }
// Returns all documents for this phase // Returns all documents for this phase
docs, err := b.Select(document.NewDeployToK8sSelector()) bundle, err := b.SelectBundle(document.NewDeployToK8sSelector())
if err != nil { if err != nil {
return err return err
} }
if len(docs) == 0 { ch := o.Applier.ApplyBundle(bundle, ao)
return document.ErrDocNotFound{} return o.Processor.Process(ch)
}
return kctl.Apply(docs, ao)
} }

View File

@ -15,19 +15,19 @@
package apply_test package apply_test
import ( import (
"errors" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/environment" "opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/k8s/client" "opendev.org/airship/airshipctl/pkg/k8s/applier"
"opendev.org/airship/airshipctl/pkg/k8s/client/fake"
"opendev.org/airship/airshipctl/pkg/k8s/kubectl"
"opendev.org/airship/airshipctl/pkg/phase/apply" "opendev.org/airship/airshipctl/pkg/phase/apply"
"opendev.org/airship/airshipctl/testutil" "opendev.org/airship/airshipctl/testutil"
"opendev.org/airship/airshipctl/testutil/k8sutils" "opendev.org/airship/airshipctl/testutil/k8sutils"
@ -38,12 +38,7 @@ const (
airshipConfigFile = "testdata/config.yaml" airshipConfigFile = "testdata/config.yaml"
) )
var (
ErrDynamicClientError = errors.New("ErrDynamicClientError")
)
func TestDeploy(t *testing.T) { func TestDeploy(t *testing.T) {
rs := makeNewFakeRootSettings(t, kubeconfigPath, airshipConfigFile)
bundle := testutil.NewTestBundle(t, "testdata/primary/site/test-site/ephemeral/initinfra") bundle := testutil.NewTestBundle(t, "testdata/primary/site/test-site/ephemeral/initinfra")
replicationController, err := bundle.SelectOne(document.NewSelector().ByKind("ReplicationController")) replicationController, err := bundle.SelectOne(document.NewSelector().ByKind("ReplicationController"))
require.NoError(t, err) require.NoError(t, err)
@ -61,44 +56,61 @@ func TestDeploy(t *testing.T) {
}, },
}) })
defer f.Cleanup() defer f.Cleanup()
ao := apply.NewOptions(rs)
ao.PhaseName = "initinfra"
ao.DryRun = true
kctl := kubectl.NewKubectl(f)
tests := []struct { tests := []struct {
theApplyOptions *apply.Options name string
client client.Interface expectedErrorString string
prune bool cliApplier *applier.Applier
expectedError error clusterPurposes map[string]*config.ClusterPurpose
phaseName string
}{ }{
{ {
name: "success",
client: fake.NewClient(fake.WithKubectl( expectedErrorString: "",
kubectl.NewKubectl(k8sutils. cliApplier: applier.NewFakeApplier(genericclioptions.IOStreams{
NewMockKubectlFactory(). In: os.Stdin,
WithDynamicClientByError(nil, ErrDynamicClientError)))), Out: os.Stdout,
expectedError: ErrDynamicClientError, ErrOut: os.Stderr,
}, k8sutils.SuccessEvents(), f),
}, },
{ {
expectedError: nil, name: "missing clusters",
prune: false, expectedErrorString: "At least one cluster needs to be defined",
client: fake.NewClient(fake.WithKubectl(kctl)), clusterPurposes: map[string]*config.ClusterPurpose{},
}, },
{ {
expectedError: nil, name: "missing phase",
prune: true, expectedErrorString: "Phase document 'missingPhase' was not found",
client: fake.NewClient(fake.WithKubectl(kctl)), phaseName: "missingPhase",
}, },
} }
for _, test := range tests { for _, tt := range tests {
ao.Prune = test.prune tt := tt
ao.Client = test.client t.Run(tt.name, func(t *testing.T) {
actualErr := ao.Run() rs := makeNewFakeRootSettings(t, kubeconfigPath, airshipConfigFile)
assert.Equal(t, test.expectedError, actualErr) ao := &apply.Options{
RootSettings: rs,
}
ao.Initialize()
ao.PhaseName = "initinfra"
ao.DryRun = true
if tt.cliApplier != nil {
ao.Applier = tt.cliApplier
}
if tt.clusterPurposes != nil {
ao.RootSettings.Config.Clusters = tt.clusterPurposes
}
if tt.phaseName != "" {
ao.PhaseName = tt.phaseName
}
actualErr := ao.Run()
if tt.expectedErrorString != "" {
require.Error(t, actualErr)
assert.Contains(t, actualErr.Error(), tt.expectedErrorString)
} else {
assert.NoError(t, actualErr)
}
})
} }
} }

View File

@ -17,10 +17,9 @@ set -xe
export KUBECONFIG=${KUBECONFIG:-"$HOME/.airship/kubeconfig"} export KUBECONFIG=${KUBECONFIG:-"$HOME/.airship/kubeconfig"}
echo "Deploy metal3.io components to ephemeral node" echo "Deploy metal3.io components to ephemeral node"
airshipctl phase apply initinfra --debug airshipctl phase apply initinfra --wait-timeout 1000s --debug
echo "Waiting for metal3 pods to come up" echo "Getting metal3 pods as debug information"
kubectl --kubeconfig $KUBECONFIG wait --for=condition=ready pods --all --timeout=1000s -A
kubectl --kubeconfig $KUBECONFIG --namespace metal3 get pods kubectl --kubeconfig $KUBECONFIG --namespace metal3 get pods
echo "Deploy cluster components to ephemeral node" echo "Deploy cluster components to ephemeral node"