Unverified Commit 70935911 authored by Oren Shomron's avatar Oren Shomron Committed by GitHub
Browse files

Merge pull request #834 from shomron/issue-625-pkg-remove

Add pkg remove command.
parents 025a3190 ddc51e08
......@@ -71,8 +71,8 @@ const (
OptionInstalled = "only-installed"
// OptionJPaths is jsonnet paths.
OptionJPaths = "jpaths"
// OptionLibName is libName.
OptionLibName = "lib-name"
// OptionPkgName is (an optionally qualified) name of a package.
OptionPkgName = "pkg-name"
// OptionName is name option.
OptionName = "name"
// OptionModule is component module option.
......
......@@ -23,7 +23,7 @@ import (
type libCacher func(app.App, registry.InstalledChecker, pkg.Descriptor, string, bool) (*app.LibraryConfig, error)
type libUpdater func(name string, env string, spec *app.LibraryConfig) error
type libUpdater func(name string, env string, spec *app.LibraryConfig) (*app.LibraryConfig, error)
// RunPkgInstall runs `pkg install`
func RunPkgInstall(m map[string]interface{}) error {
......@@ -60,7 +60,7 @@ func NewPkgInstall(m map[string]interface{}) (*PkgInstall, error) {
nl := &PkgInstall{
app: a,
libName: ol.LoadString(OptionLibName),
libName: ol.LoadString(OptionPkgName),
customName: ol.LoadString(OptionName),
force: ol.LoadBool(OptionForce),
envName: ol.LoadOptionalString(OptionEnvName),
......@@ -91,11 +91,17 @@ func (pi *PkgInstall) Run() error {
return err
}
err = pi.libUpdateFn(d.Name, pi.envName, libCfg)
oldCfg, err := pi.libUpdateFn(d.Name, pi.envName, libCfg)
if err != nil {
return err
}
if oldCfg == nil {
return nil
}
// TODO Garbage collector hook here
return nil
}
......
......@@ -33,7 +33,7 @@ func TestPkgInstall(t *testing.T) {
in := map[string]interface{}{
OptionApp: appMock,
OptionLibName: libName,
OptionPkgName: libName,
OptionName: customName,
OptionForce: false,
OptionTLSSkipVerify: false,
......@@ -60,7 +60,7 @@ func TestPkgInstall(t *testing.T) {
}
var updaterCalled bool
fakeUpdater := func(name string, env string, spec *app.LibraryConfig) error {
fakeUpdater := func(name string, env string, spec *app.LibraryConfig) (*app.LibraryConfig, error) {
updaterCalled = true
assert.Equal(t, newLibCfg.Name, name, "unexpected library name")
assert.Equal(t, a.envName, env, "unexpected environment name")
......@@ -68,7 +68,7 @@ func TestPkgInstall(t *testing.T) {
if spec != nil {
assert.Equal(t, expectedD.Name, spec.Name, "unexpected library name in configuration object")
}
return nil
return nil, nil
}
a.libCacherFn = fakeCacher
......
// 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/pkg/app"
"github.com/ksonnet/ksonnet/pkg/pkg"
"github.com/ksonnet/ksonnet/pkg/registry"
)
// PkgRemove removes packages
type PkgRemove struct {
app app.App
pkgName string
envName string
checker registry.InstalledChecker
libUpdateFn libUpdater
}
// NewPkgRemove creates an instance of PkgInstall
func NewPkgRemove(m map[string]interface{}) (*PkgRemove, error) {
ol := newOptionLoader(m)
a := ol.LoadApp()
if ol.err != nil {
return nil, ol.err
}
pr := &PkgRemove{
app: a,
pkgName: ol.LoadString(OptionPkgName),
envName: ol.LoadOptionalString(OptionEnvName),
libUpdateFn: a.UpdateLib,
}
if ol.err != nil {
return nil, ol.err
}
return pr, nil
}
// RunPkgRemove runs `pkg list`
func RunPkgRemove(m map[string]interface{}) error {
pr, err := NewPkgRemove(m)
if err != nil {
return err
}
return pr.Run()
}
// Run removes packages
func (pr *PkgRemove) Run() error {
desc, err := pkg.Parse(pr.pkgName)
if err != nil {
return err
}
oldCfg, err := pr.libUpdateFn(desc.Name, pr.envName, nil)
if err != nil {
return err
}
if oldCfg == nil {
return nil
}
// TODO: Garbage collection hook goes here
return 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 (
"testing"
"github.com/ksonnet/ksonnet/pkg/app"
amocks "github.com/ksonnet/ksonnet/pkg/app/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPkgRemove(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
libName := "incubator/apache"
in := map[string]interface{}{
OptionApp: appMock,
OptionPkgName: libName,
}
a, err := NewPkgRemove(in)
require.NoError(t, err)
var updaterCalled bool
fakeUpdater := func(name string, env string, spec *app.LibraryConfig) (*app.LibraryConfig, error) {
updaterCalled = true
assert.Equal(t, a.envName, env, "unexpected environment name")
assert.Nil(t, spec)
return nil, nil
}
a.libUpdateFn = fakeUpdater
err = a.Run()
require.NoError(t, err)
assert.True(t, updaterCalled, "library reference updater not called")
})
}
......@@ -87,10 +87,12 @@ type App interface {
SetCurrentEnvironment(name string) error
// UpdateTargets sets the targets for an environment.
UpdateTargets(envName string, targets []string) error
// UpdateLib adds or updates a library reference.
// UpdateLib adds, updates or removes a library reference.
// env is optional - if provided the reference is scoped under the environment,
// otherwise it is globally scoped.
UpdateLib(name string, env string, spec *LibraryConfig) error
// If spec if nil, the library reference will be removed.
// Returns the previous reference for the named library, if one existed.
UpdateLib(name string, env string, spec *LibraryConfig) (*LibraryConfig, error)
// UpdateRegistry updates a registry.
UpdateRegistry(spec *RegistryConfig) error
// Upgrade upgrades an application to the current version.
......
......@@ -236,44 +236,67 @@ func (ba *baseApp) AddRegistry(newReg *RegistryConfig, isOverride bool) error {
// UpdateLib adds or updates a library reference.
// env is optional - if provided the reference is scoped under the environment,
// otherwise it is globally scoped.
func (ba *baseApp) UpdateLib(name string, env string, libSpec *LibraryConfig) error {
// If spec if nil, the library reference will be removed.
// Returns the previous reference for the named library, if one existed.
func (ba *baseApp) UpdateLib(name string, env string, libSpec *LibraryConfig) (*LibraryConfig, error) {
if err := ba.load(); err != nil {
return errors.Wrap(err, "load configuration")
return nil, errors.Wrap(err, "load configuration")
}
if ba.config == nil {
return errors.Errorf("invalid app - configuration is nil")
return nil, errors.Errorf("invalid app - configuration is nil")
}
if libSpec.Name != name {
return errors.Errorf("library name mismatch: %v vs %v", libSpec.Name, name)
if libSpec != nil && libSpec.Name != name {
return nil, errors.Errorf("library name mismatch: %v vs %v", libSpec.Name, name)
}
// TODO support app overrides
var oldSpec *LibraryConfig
switch env {
case "":
if ba.config.Libraries == nil {
ba.config.Libraries = LibraryConfigs{}
}
// Globally scoped
ba.config.Libraries[name] = libSpec
oldSpec = ba.config.Libraries[name]
if libSpec == nil {
if oldSpec == nil {
return nil, errors.Errorf("package not found: %s", name)
}
delete(ba.config.Libraries, name)
} else {
ba.config.Libraries[name] = libSpec
}
default:
// Scoped by environment
e, ok := ba.config.GetEnvironmentConfig(env)
if !ok {
return errors.Errorf("invalid environment: %v", env)
return nil, errors.Errorf("invalid environment: %v", env)
}
if e.Libraries == nil {
// We may want to move this into EnvrionmentConfig unmarshaling code.
e.Libraries = LibraryConfigs{}
}
e.Libraries[name] = libSpec
oldSpec = e.Libraries[name]
if libSpec == nil {
if oldSpec == nil {
return nil, errors.Errorf("package %s not found in environment: %s", name, env)
}
delete(e.Libraries, name)
} else {
e.Libraries[name] = libSpec
}
if err := ba.config.UpdateEnvironmentConfig(env, e); err != nil {
return errors.Wrapf(err, "updating environment %v", env)
return nil, errors.Wrapf(err, "updating environment %v", env)
}
}
return ba.save()
return oldSpec, ba.save()
}
// UpdateRegistry updates a registry spec and persists in app[.override].yaml
......
......@@ -245,7 +245,7 @@ func Test_baseApp_UpdateLibrary(t *testing.T) {
ba := newBaseApp(fs, "/", nil)
// Test updating non-existing registry
err := ba.UpdateLib(tc.libCfg.Name, tc.env, &tc.libCfg)
_, err := ba.UpdateLib(tc.libCfg.Name, tc.env, &tc.libCfg)
if tc.expectErr {
require.Error(t, err)
} else {
......@@ -259,6 +259,148 @@ func Test_baseApp_UpdateLibrary(t *testing.T) {
}
func Test_baseApp_UpdateLib_Remove(t *testing.T) {
tests := []struct {
name string
globalLibraries LibraryConfigs
environments EnvironmentConfigs
libName string
expectRemovedLib *LibraryConfig
env string
appFilePath string
expectEnvironments EnvironmentConfigs
expectLibraries LibraryConfigs
expectErr bool
}{
{
name: "remove - env - exists",
libName: "nginx",
env: "default",
appFilePath: "app020_simple.yaml",
environments: EnvironmentConfigs{
"default": &EnvironmentConfig{
Name: "default",
Libraries: LibraryConfigs{
"nginx": &LibraryConfig{
Name: "nginx",
Registry: "incubator",
Version: "1.2.3",
},
"other": &LibraryConfig{
Name: "other",
Registry: "incubator",
Version: "1.2.3",
},
},
},
},
expectRemovedLib: &LibraryConfig{
Name: "nginx",
Registry: "incubator",
Version: "1.2.3",
},
expectEnvironments: EnvironmentConfigs{
"default": &EnvironmentConfig{
Name: "default",
Libraries: LibraryConfigs{
"other": &LibraryConfig{
Name: "other",
Registry: "incubator",
Version: "1.2.3",
},
},
},
},
},
{
name: "remove - global - exists",
libName: "nginx",
appFilePath: "app020_simple.yaml",
globalLibraries: LibraryConfigs{
"nginx": &LibraryConfig{
Name: "nginx",
Registry: "incubator",
Version: "1.2.3",
},
"other": &LibraryConfig{
Name: "other",
Registry: "incubator",
Version: "1.2.3",
},
},
expectRemovedLib: &LibraryConfig{
Name: "nginx",
Registry: "incubator",
Version: "1.2.3",
},
expectLibraries: LibraryConfigs{
"other": &LibraryConfig{
Name: "other",
Registry: "incubator",
Version: "1.2.3",
},
},
expectErr: false,
},
{
name: "no such environment",
libName: "nginx",
env: "no-such-environment",
appFilePath: "app020_app.yaml",
expectErr: true,
},
{
name: "no such library",
libName: "no-such-library",
appFilePath: "app020_app.yaml",
globalLibraries: LibraryConfigs{
"nginx": &LibraryConfig{
Name: "nginx",
Registry: "incubator",
Version: "1.2.3",
},
"other": &LibraryConfig{
Name: "other",
Registry: "incubator",
Version: "1.2.3",
},
},
expectErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fs := afero.NewMemMapFs()
if tc.appFilePath != "" {
stageFile(t, fs, tc.appFilePath, "/app.yaml")
}
ba := newBaseApp(fs, "/", nil)
ba.load = func() error { return nil }
if tc.globalLibraries != nil {
ba.config.Libraries = tc.globalLibraries
}
if tc.environments != nil {
ba.config.Environments = tc.environments
}
removed, err := ba.UpdateLib(tc.libName, tc.env, nil)
if tc.expectErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tc.expectRemovedLib, removed)
assert.Equal(t, tc.expectLibraries, ba.config.Libraries)
assert.Equal(t, tc.expectEnvironments, ba.config.Environments)
})
}
}
func Test_baseApp_load_override(t *testing.T) {
fs := afero.NewMemMapFs()
......@@ -375,3 +517,4 @@ func Test_baseApp_environment_just_override(t *testing.T) {
assert.Equal(t, expected, e)
}
......@@ -312,17 +312,26 @@ func (_m *App) SetCurrentEnvironment(name string) error {
}
// UpdateLib provides a mock function with given fields: name, env, spec
func (_m *App) UpdateLib(name string, env string, spec *app.LibraryConfig) error {
func (_m *App) UpdateLib(name string, env string, spec *app.LibraryConfig) (*app.LibraryConfig, error) {
ret := _m.Called(name, env, spec)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, *app.LibraryConfig) error); ok {
var r0 *app.LibraryConfig
if rf, ok := ret.Get(0).(func(string, string, *app.LibraryConfig) *app.LibraryConfig); ok {
r0 = rf(name, env, spec)
} else {
r0 = ret.Error(0)
if ret.Get(0) != nil {
r0 = ret.Get(0).(*app.LibraryConfig)
}
}
return r0
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *app.LibraryConfig) error); ok {
r1 = rf(name, env, spec)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateRegistry provides a mock function with given fields: spec
......
apiVersion: 0.2.0
environments:
default:
libraries:
kind: ksonnet.io/app
libraries:
name: test-get-envs
version: 0.0.1
......@@ -48,6 +48,7 @@ const (
actionPkgDescribe
actionPkgInstall
actionPkgList
actionPkgRemove
actionPrototypeDescribe
actionPrototypeList
actionPrototypePreview
......@@ -91,6 +92,7 @@ var (
actionPkgDescribe: actions.RunPkgDescribe,
actionPkgInstall: actions.RunPkgInstall,
actionPkgList: actions.RunPkgList,
actionPkgRemove: actions.RunPkgRemove,
actionPrototypeDescribe: actions.RunPrototypeDescribe,
actionPrototypeList: actions.RunPrototypeList,
actionPrototypePreview: actions.RunPrototypePreview,
......
......@@ -27,6 +27,7 @@ const (
var (
pkgShortDesc = map[string]string{
"install": "Install a package (e.g. extra prototypes) for the current ksonnet app",
"remove": "Remove a package from the app or environment scope",
"describe": "Describe a ksonnet package and its contents",
"list": "List all packages known (downloaded or not) for the current ksonnet app",
}
......@@ -68,6 +69,7 @@ func newPkgCmd(a app.App) *cobra.Command {
pkgCmd.AddCommand(newPkgListCmd(a))
pkgCmd.AddCommand(newPkgInstallCmd(a))
pkgCmd.AddCommand(newPkgDescribeCmd(a))
pkgCmd.AddCommand(newPkgRemoveCmd(a))
return pkgCmd
}
......@@ -80,7 +80,7 @@ func newPkgInstallCmd(a app.App) *cobra.Command {
m := map[string]interface{}{
actions.OptionApp: a,
actions.OptionLibName: args[0],
actions.OptionPkgName: args[0],
actions.OptionName: viper.GetString(vPkgInstallName),
actions.OptionEnvName: viper.GetString(vPkgInstallEnv),
actions.OptionForce: viper.GetBool(vPkgInstallForce),
......
......@@ -29,7 +29,7 @@ func Test_pkgInstallCmd(t *testing.T) {
action: actionPkgInstall,
expected: map[string]interface{}{
actions.OptionApp: nil,
actions.OptionLibName: "package-name",
actions.OptionPkgName: "package-name",
actions.OptionName: "",
actions.OptionEnvName: "",
actions.OptionForce: false,
......@@ -42,7 +42,7 @@ func Test_pkgInstallCmd(t *testing.T) {
action: actionPkgInstall,
expected: map[string]interface{}{
actions.OptionApp: nil,
actions.OptionLibName: "package-name",
actions.OptionPkgName: "package-name",
actions.OptionName: "",
actions.OptionEnvName: "production",
actions.OptionForce: false,
......@@ -55,7 +55,7 @@ func Test_pkgInstallCmd(t *testing.T) {
action: actionPkgInstall,
expected: map[string]interface{}{
actions.OptionApp: nil,
actions.OptionLibName: "package-name",
actions.OptionPkgName: "package-name",
actions.OptionName: "",
actions.OptionEnvName: "",
actions.OptionForce: true,
......
// 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 clicmd
import (
"fmt"
"github.com/spf13/viper"
"github.com/ksonnet/ksonnet/pkg/actions"
"github.com/ksonnet/ksonnet/pkg/app"
"github.com/spf13/cobra"
)
var (
vPkgRemoveEnv = "pkg-remove-env"