Commit ddc51e08 authored by Oren Shomron's avatar Oren Shomron
Browse files

Add pkg remove command.



Removes package references from either global or environment scope.
Garbage collection for orphaned packages will be implemented separately.

Closes #625
Signed-off-by: default avatarOren Shomron <shomron@gmail.com>
parent 025a3190
......@@ -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"
pkgRemoveLong = `