Refact cwhub: item removal with shared dependencies (#2598)
* Iterate over sub-items in Remove(), not in disable() -- fix shared dependency issue * Increase hub download timeout to 2 minutes
This commit is contained in:
parent
65473d4e05
commit
56ad2bbf98
7 changed files with 107 additions and 33 deletions
|
@ -10,5 +10,5 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var hubClient = &http.Client{
|
var hubClient = &http.Client{
|
||||||
Timeout: 20 * time.Second,
|
Timeout: 120 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,35 +146,10 @@ func (i *Item) removeInstallLink() error {
|
||||||
// disable removes the symlink to the downloaded content, also removes the content if purge is true
|
// disable removes the symlink to the downloaded content, also removes the content if purge is true
|
||||||
func (i *Item) disable(purge bool, force bool) error {
|
func (i *Item) disable(purge bool, force bool) error {
|
||||||
// XXX: should return the number of disabled/purged items to inform the upper layer whether to reload or not
|
// XXX: should return the number of disabled/purged items to inform the upper layer whether to reload or not
|
||||||
if i.IsLocal() {
|
|
||||||
return fmt.Errorf("%s isn't managed by hub. Please delete manually", i.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if i.Tainted && !force {
|
|
||||||
return fmt.Errorf("%s is tainted, use '--force' to overwrite", i.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sub := range i.SubItems() {
|
|
||||||
// TODO XXX: if the other collection(s) are direct or indirect dependencies of the current one, it's good to go
|
|
||||||
if len(sub.BelongsToCollections) > 1 {
|
|
||||||
log.Infof("%s was not removed because it belongs to another collection", sub.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sub.disable(purge, force); err != nil {
|
|
||||||
return fmt.Errorf("while disabling %s: %w", sub.Name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !i.Installed && !purge {
|
|
||||||
log.Infof("removing %s: not installed -- no need to remove", i.Name)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := i.removeInstallLink()
|
err := i.removeInstallLink()
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if !purge && !force {
|
if !purge && !force {
|
||||||
return fmt.Errorf("can't disable %s: %s doesn't exist", i.Name, i.installLink())
|
return fmt.Errorf("link %s does not exist (override with --force or --purge)", i.installLink())
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
|
|
||||||
"github.com/enescakir/emoji"
|
"github.com/enescakir/emoji"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Install installs the item from the hub, downloading it if needed
|
// Install installs the item from the hub, downloading it if needed
|
||||||
|
@ -51,12 +52,70 @@ func (i *Item) Install(force bool, downloadOnly bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allDependencies return a list of all dependencies and sub-dependencies of the item
|
||||||
|
func (i *Item) allDependencies() []*Item {
|
||||||
|
var deps []*Item
|
||||||
|
|
||||||
|
for _, dep := range i.SubItems() {
|
||||||
|
if dep == i {
|
||||||
|
log.Errorf("circular dependency detected: %s depends on %s", dep.Name, i.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deps = append(deps, dep.allDependencies()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(deps, i)
|
||||||
|
}
|
||||||
|
|
||||||
// Remove disables the item, optionally removing the downloaded content
|
// Remove disables the item, optionally removing the downloaded content
|
||||||
func (i *Item) Remove(purge bool, forceAction bool) (bool, error) {
|
func (i *Item) Remove(purge bool, force bool) (bool, error) {
|
||||||
|
if i.IsLocal() {
|
||||||
|
return false, fmt.Errorf("%s isn't managed by hub. Please delete manually", i.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.Tainted && !force {
|
||||||
|
return false, fmt.Errorf("%s is tainted, use '--force' to remove", i.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !i.Installed && !purge {
|
||||||
|
log.Infof("removing %s: not installed -- no need to remove", i.Name)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
removed := false
|
removed := false
|
||||||
|
|
||||||
if err := i.disable(purge, forceAction); err != nil {
|
allDeps := i.allDependencies()
|
||||||
return false, fmt.Errorf("unable to disable %s: %w", i.Name, err)
|
|
||||||
|
for _, sub := range i.SubItems() {
|
||||||
|
if !sub.Installed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the other collection(s) are direct or indirect dependencies of the current one, it's good to go
|
||||||
|
// log parent collections
|
||||||
|
for _, subParent := range sub.parentCollections() {
|
||||||
|
if subParent == i {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.Contains(allDeps, subParent) {
|
||||||
|
log.Infof("%s was not removed because it also belongs to %s", sub.Name, subParent.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subRemoved, err := sub.Remove(purge, force)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("unable to disable %s: %w", i.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
removed = removed || subRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
err := i.disable(purge, force)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("while removing %s: %w", i.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: should take the value from disable()
|
// XXX: should take the value from disable()
|
||||||
|
|
|
@ -186,6 +186,21 @@ func (i *Item) logMissingSubItems() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Item) parentCollections() []*Item {
|
||||||
|
ret := make([]*Item, 0)
|
||||||
|
|
||||||
|
for _, parentName := range i.BelongsToCollections {
|
||||||
|
parent := i.hub.GetItem(COLLECTIONS, parentName)
|
||||||
|
if parent == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
// Status returns the status of the item as a string and an emoji
|
// Status returns the status of the item as a string and an emoji
|
||||||
// ie. "enabled,update-available" and emoji.Warning
|
// ie. "enabled,update-available" and emoji.Warning
|
||||||
func (i *Item) Status() (string, emoji.Emoji) {
|
func (i *Item) Status() (string, emoji.Emoji) {
|
||||||
|
|
|
@ -393,6 +393,8 @@ func (h *Hub) syncDir(dir string) ([]string, error) {
|
||||||
|
|
||||||
// For each, scan PARSERS, POSTOVERFLOWS, SCENARIOS and COLLECTIONS last
|
// For each, scan PARSERS, POSTOVERFLOWS, SCENARIOS and COLLECTIONS last
|
||||||
for _, scan := range ItemTypes {
|
for _, scan := range ItemTypes {
|
||||||
|
// cpath: top-level item directory, either downloaded or installed items.
|
||||||
|
// i.e. /etc/crowdsec/parsers, /etc/crowdsec/hub/parsers, ...
|
||||||
cpath, err := filepath.Abs(fmt.Sprintf("%s/%s", dir, scan))
|
cpath, err := filepath.Abs(fmt.Sprintf("%s/%s", dir, scan))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed %s: %s", cpath, err)
|
log.Errorf("failed %s: %s", cpath, err)
|
||||||
|
|
|
@ -81,11 +81,34 @@ teardown() {
|
||||||
|
|
||||||
# now we can't remove smb without --force
|
# now we can't remove smb without --force
|
||||||
rune -1 cscli collections remove crowdsecurity/smb
|
rune -1 cscli collections remove crowdsecurity/smb
|
||||||
assert_stderr --partial "unable to disable crowdsecurity/smb: crowdsecurity/smb is tainted, use '--force' to overwrite"
|
assert_stderr --partial "crowdsecurity/smb is tainted, use '--force' to remove"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "cscli collections (dependencies II: the revenge)" {
|
||||||
rune -0 cscli collections install crowdsecurity/wireguard baudneo/gotify
|
rune -0 cscli collections install crowdsecurity/wireguard baudneo/gotify
|
||||||
rune -0 cscli collections remove crowdsecurity/wireguard
|
rune -0 cscli collections remove crowdsecurity/wireguard
|
||||||
assert_stderr --partial "crowdsecurity/syslog-logs was not removed because it belongs to another collection"
|
assert_stderr --partial "crowdsecurity/syslog-logs was not removed because it also belongs to baudneo/gotify"
|
||||||
rune -0 cscli collections inspect crowdsecurity/wireguard -o json
|
rune -0 cscli collections inspect crowdsecurity/wireguard -o json
|
||||||
rune -0 jq -e '.installed==false' <(output)
|
rune -0 jq -e '.installed==false' <(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "cscli collections (dependencies III: origins)" {
|
||||||
|
# it is perfectly fine to remove an item belonging to a collection that we are removing anyway
|
||||||
|
|
||||||
|
# inject a dependency: sshd requires the syslog-logs parsers, but linux does too
|
||||||
|
hub_dep=$(jq <"$INDEX_PATH" '. * {collections:{"crowdsecurity/sshd":{parsers:["crowdsecurity/syslog-logs"]}}}')
|
||||||
|
echo "$hub_dep" >"$INDEX_PATH"
|
||||||
|
|
||||||
|
# verify that installing sshd brings syslog-logs
|
||||||
|
rune -0 cscli collections install crowdsecurity/sshd
|
||||||
|
rune -0 cscli parsers inspect crowdsecurity/syslog-logs -o json
|
||||||
|
rune -0 jq -e '.installed==true' <(output)
|
||||||
|
|
||||||
|
rune -0 cscli collections install crowdsecurity/linux
|
||||||
|
|
||||||
|
# removing linux should remove syslog-logs even though sshd depends on it
|
||||||
|
rune -0 cscli collections remove crowdsecurity/linux
|
||||||
|
refute_stderr --partial "crowdsecurity/syslog-logs was not removed"
|
||||||
|
rune -0 cscli parsers list -o json
|
||||||
|
rune -0 jq -e '.parsers | length == 0' <(output)
|
||||||
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ teardown() {
|
||||||
rune -0 rm "$(output)"
|
rune -0 rm "$(output)"
|
||||||
|
|
||||||
rune -0 cscli parsers remove crowdsecurity/syslog-logs --debug
|
rune -0 cscli parsers remove crowdsecurity/syslog-logs --debug
|
||||||
assert_stderr --partial "Removed crowdsecurity/syslog-logs"
|
assert_stderr --partial "removing crowdsecurity/syslog-logs: not installed -- no need to remove"
|
||||||
|
|
||||||
rune -0 cscli parsers inspect crowdsecurity/syslog-logs -o json
|
rune -0 cscli parsers inspect crowdsecurity/syslog-logs -o json
|
||||||
rune -0 jq -r '.path' <(output)
|
rune -0 jq -r '.path' <(output)
|
||||||
|
|
Loading…
Reference in a new issue