Unverified Commit 352bbaa3 authored by Bryan Liles's avatar Bryan Liles Committed by GitHub
Browse files

Merge pull request #414 from bryanl/update-delete-action

Update delete action
parents 72110c1c 755b7de0
......@@ -35,6 +35,12 @@
revision = "2ee87856327ba09384cabd113bc6b5d174e9ec0f"
version = "v3.5.1"
[[projects]]
name = "github.com/cenkalti/backoff"
packages = ["."]
revision = "2ea60e5f094469f9e65adb9cd103795b73ae743e"
version = "v2.0.0"
[[projects]]
name = "github.com/cpuguy83/go-md2man"
packages = ["md2man"]
......@@ -669,6 +675,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "b1ae87ac0ef11d1061f455055e4c56dc7e39a753d77347d2e02af4d44e6a7633"
inputs-digest = "12c28be36adc447c9dfceffe28256a3ec5c0709379755082872b981f309857ee"
solver-name = "gps-cdcl"
solver-version = 1
......@@ -47,6 +47,8 @@ const (
OptionGcTag = "gc-tag"
// OptionGlobal is global option.
OptionGlobal = "global"
// OptionGracePeriod is gracePeriod option.
OptionGracePeriod = "grace-period"
// OptionIndex is index option. Is used to target individual items in multi object
// components.
OptionIndex = "index"
......@@ -194,6 +196,21 @@ func (o *optionLoader) loadInt(name string) int {
return a
}
func (o *optionLoader) loadInt64(name string) int64 {
i := o.load(name)
if i == nil {
return 0
}
a, ok := i.(int64)
if !ok {
o.err = newInvalidOptionError(name)
return 0
}
return a
}
func (o *optionLoader) loadOptionalInt(name string) int {
i := o.loadOptional(name)
if i == nil {
......
// Copyright 2018 The ksonnet authors
//
//
// 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
//
// http://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 actions
import (
"github.com/ksonnet/ksonnet/client"
"github.com/ksonnet/ksonnet/metadata/app"
"github.com/ksonnet/ksonnet/pkg/cluster"
)
type runDeleteFn func(cluster.DeleteConfig, ...cluster.DeleteOpts) error
// RunDelete runs `delete`.
func RunDelete(m map[string]interface{}) error {
a, err := newDelete(m)
if err != nil {
return err
}
return a.run()
}
type deleteOpt func(*Delete)
// Delete collects options for applying objects to a cluster.
type Delete struct {
app app.App
clientConfig *client.Config
componentNames []string
envName string
gracePeriod int64
runDeleteFn runDeleteFn
}
// RunDelete runs `apply`
func newDelete(m map[string]interface{}, opts ...deleteOpt) (*Delete, error) {
ol := newOptionLoader(m)
a := &Delete{
app: ol.loadApp(),
clientConfig: ol.loadClientConfig(),
componentNames: ol.loadStringSlice(OptionComponentNames),
envName: ol.loadString(OptionEnvName),
gracePeriod: ol.loadInt64(OptionGracePeriod),
runDeleteFn: cluster.RunDelete,
}
if ol.err != nil {
return nil, ol.err
}
for _, opt := range opts {
opt(a)
}
return a, nil
}
func (a *Delete) run() error {
config := cluster.DeleteConfig{
App: a.app,
ClientConfig: a.clientConfig,
ComponentNames: a.componentNames,
EnvName: a.envName,
GracePeriod: a.gracePeriod,
}
return a.runDeleteFn(config)
}
// Copyright 2018 The ksonnet authors
//
//
// 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
//
// http://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 actions
import (
"testing"
"github.com/ksonnet/ksonnet/client"
amocks "github.com/ksonnet/ksonnet/metadata/app/mocks"
"github.com/ksonnet/ksonnet/pkg/cluster"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDelete(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
in := map[string]interface{}{
OptionApp: appMock,
OptionClientConfig: &client.Config{},
OptionComponentNames: []string{},
OptionEnvName: "default",
OptionGracePeriod: int64(3),
}
expected := cluster.DeleteConfig{
App: appMock,
ClientConfig: &client.Config{},
ComponentNames: []string{},
EnvName: "default",
GracePeriod: 3,
}
runDeleteOpt := func(a *Delete) {
a.runDeleteFn = func(config cluster.DeleteConfig, opts ...cluster.DeleteOpts) error {
assert.Equal(t, expected, config)
return nil
}
}
a, err := newDelete(in, runDeleteOpt)
require.NoError(t, err)
err = a.run()
require.NoError(t, err)
})
}
func TestDelete_invalid_input(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
in := map[string]interface{}{
OptionClientConfig: "invalid",
}
_, err := newDelete(in)
require.Error(t, err)
})
}
......@@ -65,7 +65,7 @@ var (
actionApply: actions.RunApply,
actionComponentList: actions.RunComponentList,
actionComponentRm: actions.RunComponentRm,
// actionDelete
actionDelete: actions.RunDelete,
// actionDiff
actionEnvAdd: actions.RunEnvAdd,
actionEnvDescribe: actions.RunEnvDescribe,
......
......@@ -17,30 +17,40 @@ package cmd
import (
"fmt"
"os"
"github.com/spf13/viper"
"github.com/spf13/cobra"
"github.com/ksonnet/ksonnet/actions"
"github.com/ksonnet/ksonnet/client"
"github.com/ksonnet/ksonnet/pkg/kubecfg"
)
const (
flagGracePeriod = "grace-period"
deleteShortDesc = "Remove component-specified Kubernetes resources from remote clusters"
)
const (
vDeleteComponent = "delete-components"
vDeleteGracePeriod = "delete-grace-period"
)
var (
deleteClientConfig *client.Config
)
func init() {
RootCmd.AddCommand(deleteCmd)
addEnvCmdFlags(deleteCmd)
deleteClientConfig = client.NewDefaultClientConfig()
deleteClientConfig.BindClientGoFlags(deleteCmd)
bindJsonnetFlags(deleteCmd)
deleteCmd.PersistentFlags().Int64(flagGracePeriod, -1, "Number of seconds given to resources to terminate gracefully. A negative value is ignored")
deleteCmd.Flags().StringSliceP(flagComponent, shortComponent, nil, "Name of a specific component (multiple -c flags accepted, allows YAML, JSON, and Jsonnet)")
viper.BindPFlag(vDeleteComponent, deleteCmd.Flags().Lookup(flagComponent))
deleteCmd.Flags().Int64(flagGracePeriod, -1, "Number of seconds given to resources to terminate gracefully. A negative value is ignored")
viper.BindPFlag(vDeleteGracePeriod, deleteCmd.Flags().Lookup(flagGracePeriod))
}
var deleteCmd = &cobra.Command{
......@@ -50,43 +60,16 @@ var deleteCmd = &cobra.Command{
if len(args) != 1 {
return fmt.Errorf("'delete' requires an environment name; use `env list` to see available environments\n\n%s", cmd.UsageString())
}
env := args[0]
flags := cmd.Flags()
var err error
c := kubecfg.DeleteCmd{App: ka}
c.GracePeriod, err = flags.GetInt64(flagGracePeriod)
if err != nil {
return err
}
componentNames, err := flags.GetStringSlice(flagComponent)
if err != nil {
return err
}
cwd, err := os.Getwd()
if err != nil {
return err
}
c.ClientConfig = deleteClientConfig
c.Env = env
te := newCmdObjExpander(cmdObjExpanderConfig{
cmd: cmd,
env: env,
components: componentNames,
cwd: cwd,
})
objs, err := te.Expand()
if err != nil {
return err
m := map[string]interface{}{
actions.OptionApp: ka,
actions.OptionClientConfig: deleteClientConfig,
actions.OptionComponentNames: viper.GetStringSlice(vDeleteComponent),
actions.OptionEnvName: args[0],
actions.OptionGracePeriod: viper.GetInt64(vDeleteGracePeriod),
}
return c.Run(objs)
return runAction(actionDelete, m)
},
Long: `
The ` + "`delete`" + ` command removes Kubernetes resources (described in local
......
// Copyright 2018 The ksonnet authors
//
//
// 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
//
// http://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 cmd
import (
"testing"
"github.com/ksonnet/ksonnet/actions"
)
func Test_deleteCmd(t *testing.T) {
cases := []cmdTestCase{
{
name: "with no options",
args: []string{"delete", "default"},
action: actionDelete,
expected: map[string]interface{}{
actions.OptionApp: nil,
actions.OptionEnvName: "default",
actions.OptionComponentNames: make([]string, 0),
actions.OptionClientConfig: deleteClientConfig,
actions.OptionGracePeriod: int64(-1),
},
},
{
name: "with no env",
args: []string{"delete"},
action: actionDelete,
isErr: true,
},
}
runTestCmd(t, cases)
}
......@@ -28,6 +28,7 @@ const (
flagExtVarFile = "ext-str-file"
flagFilename = "filename"
flagGcTag = "gc-tag"
flagGracePeriod = "grace-period"
flagIndex = "index"
flagJpath = "jpath"
flagModule = "module"
......
// Copyright 2018 The ksonnet authors
//
//
// 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
//
// http://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.
// +build e2e
package e2e
import (
. "github.com/onsi/ginkgo"
)
var _ = Describe("ks delete", func() {
var a app
var namespace string
var o *output
BeforeEach(func() {
namespace = e.createNamespace()
io := &initOptions{
context: "gke_bryan-heptio_us-central1-a_dev2",
namespace: namespace,
}
a = e.initApp(io)
a.generateDeployedService()
o := a.runKs("apply", "default")
assertExitStatus(o, 0)
})
AfterEach(func() {
e.removeNamespace(namespace)
})
Context("deleting all items in a module", func() {
JustBeforeEach(func() {
o = a.runKs("delete", "default")
assertExitStatus(o, 0)
})
It("reports which resources it deleting", func() {
assertExitStatus(o, 0)
assertOutput("delete/output.txt", o.stderr)
})
It("deletes guestbook-ui service", func() {
v := newValidator(e.restConfig, namespace)
v.hasNoService("guestbook-ui")
})
It("deletes guestbook-ui deployment", func() {
v := newValidator(e.restConfig, namespace)
v.hasNoDeployment("guestbook-ui")
})
})
})
Deleting services guestbook-ui
Deleting deployments guestbook-ui
unable to find namespace "bad"
unable to find module "bad"
......@@ -16,6 +16,10 @@
package e2e
import (
"time"
"github.com/cenkalti/backoff"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
extv1beta1 "k8s.io/client-go/kubernetes/typed/extensions/v1beta1"
......@@ -44,10 +48,49 @@ func (v *validator) hasService(name string) {
ExpectWithOffset(1, err).NotTo(HaveOccurred())
}
func (v *validator) hasNoService(name string) {
c, err := corev1.NewForConfig(v.config)
ExpectWithOffset(1, err).NotTo(HaveOccurred())
operation := func() error {
_, err = c.Services(v.namespace).Get(name, metav1.GetOptions{})
if err == nil {
return errors.Errorf("expected service %s to not exist", name)
}
return nil
}
bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = 30 * time.Second
err = backoff.Retry(operation, bo)
ExpectWithOffset(1, err).ToNot(HaveOccurred())
}
func (v *validator) hasDeployment(name string) {
c, err := extv1beta1.NewForConfig(v.config)
Expect(err).NotTo(HaveOccurred())
ExpectWithOffset(1, err).NotTo(HaveOccurred())
_, err = c.Deployments(v.namespace).Get(name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
ExpectWithOffset(1, err).NotTo(HaveOccurred())
}
func (v *validator) hasNoDeployment(name string) {
c, err := extv1beta1.NewForConfig(v.config)
ExpectWithOffset(1, err).NotTo(HaveOccurred())
operation := func() error {
_, err = c.Deployments(v.namespace).Get(name, metav1.GetOptions{})
if err == nil {
return errors.Errorf("expected deployment %s to not exist", name)
}
return nil
}
bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = 1 * time.Minute
bo.MaxInterval = 500 * time.Millisecond
err = backoff.Retry(operation, bo)
ExpectWithOffset(1, err).ToNot(HaveOccurred())
}
// Copyright 2018 The ksonnet authors
//
//
// 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
//
// http://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 cluster
import (
"fmt"
"sort"
"github.com/ksonnet/ksonnet/client"
"github.com/ksonnet/ksonnet/metadata/app"
"github.com/ksonnet/ksonnet/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// DeleteConfig is configuration for Delete.
type DeleteConfig struct {
App app.App
ClientConfig *client.Config
ComponentNames []string
EnvName string
GracePeriod int64
}
// DeleteOpts is an option for configuring Delete.
type DeleteOpts func(*Delete)
// Delete deletes objects from the cluster.
type Delete struct {
DeleteConfig
// these make it easier to test Delete.
findObjectsFn findObjectsFn
genClientOptsFn genClientOptsFn
objectInfo ObjectInfo
resourceClientFactory resourceClientFactoryFn
}
// RunDelete runs delete against a cluster for a given configuration.
func RunDelete(config DeleteConfig, opts ...DeleteOpts) error {
d := &Delete{
DeleteConfig: config,
findObjectsFn: findObjects,
genClientOptsFn: genClientOpts,
resourceClientFactory: resourceClientFactory,
objectInfo: &objectInfo{},
}
for _, opt := range opts {
opt(d)
}
return d.Delete()
}
// Delete deletes objects from a cluster.
func (d *Delete) Delete() error {
apiObjects, err := d.findObjectsFn(d.App, d.EnvName, d.ComponentNames)
if err != nil {
return errors.Wrap(err, "find objects")
}
co, err := d.genClientOptsFn(d.App, d.ClientConfig, d.EnvName)
if err != nil {
return err
}
version, err := utils.FetchVersion(co.discovery)
if err != nil {
return err
}
sort.Sort(sort.Reverse(utils.DependencyOrder(apiObjects)))
deleteOpts := metav1.DeleteOptions{}
if version.Compare(1, 6) < 0 {
// 1.5.x option
boolFalse := false
deleteOpts.OrphanDependents = &boolFalse
} else {
// 1.6.x option (NB: Background is broken)
fg := metav1.DeletePropagationForeground
deleteOpts.PropagationPolicy = &fg
}
if d.GracePeriod >= 0 {
deleteOpts.GracePeriodSeconds = &d.GracePeriod
}
for _, obj := range apiObjects {
desc := fmt.Sprintf("%s %s", d.objectInfo.ResourceName(co.discovery, obj), utils.FqName(obj))
log.Info("Deleting ", desc)
client, err := d.resourceClientFactory(co, obj)
if err != nil {
return err
}