Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion cmd/nerdctl/system_prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
package main

import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"github.com/containerd/nerdctl/pkg/buildkitutil"
"strings"

"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -72,6 +76,7 @@ func systemPruneAction(cmd *cobra.Command, args []string) error {
}
msg += `
- all images without at least one container associated to them
- all build cache
`
msg += "\nAre you sure you want to continue? [y/N] "
fmt.Fprintf(cmd.OutOrStdout(), "WARNING! %s", msg)
Expand Down Expand Up @@ -99,5 +104,29 @@ func systemPruneAction(cmd *cobra.Command, args []string) error {
return err
}
}
return imagePrune(cmd, client, ctx)
if err := imagePrune(cmd, client, ctx); err != nil {
return err
}

var buf bytes.Buffer
outWriter := bufio.NewWriter(&buf)
copiedCmd := *cmd
copiedCmd.SetOut(outWriter)
if err := builderPruneAction(&copiedCmd, args); err != nil {
Comment thread
STRRL marked this conversation as resolved.
return err
}
parsedOutput, err := buildkitutil.ParseBuildctlPruneTableOutput(buf.Bytes())
if err != nil {
return err
}
// pretty print the output of prune build cache
out := cmd.OutOrStdout()
for _, row := range parsedOutput.Rows {
_, _ = fmt.Fprintf(out, "%s\n", row.ID)
}
j, _ := json.Marshal(parsedOutput)
fmt.Fprintf(out, "%s\n", j)

fmt.Fprintf(out, "%s", buf.Bytes())
return err
}
97 changes: 97 additions & 0 deletions pkg/buildkitutil/buildctl_prune_output_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
Copyright The containerd 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.
*/

/*
Portions from https://github.com/docker/cli/blob/v20.10.9/cli/command/image/build/context.go
Copyright (C) Docker authors.
Licensed under the Apache License, Version 2.0
NOTICE: https://github.com/docker/cli/blob/v20.10.9/NOTICE
*/

package buildkitutil

import (
"fmt"
"github.com/containerd/nerdctl/pkg/tabutil"
"strings"
)

type BuildctlPruneOutput struct {
TotalSize string
Rows []BuildctlPruneOutputRow
}

type BuildctlPruneOutputRow struct {
ID string
Reclaimable string
Size string
LastAccessed string
}

const HeaderID = "ID"
const HeaderReclaimable = "RECLAIMABLE"
const HeaderSize = "SIZE"
const HeaderLastAccessed = "LAST ACCESSED"
const FinalizerTotal = "Total:"

func ParseBuildctlPruneTableOutput(out []byte) (*BuildctlPruneOutput, error) {
tabReader := tabutil.NewReader(fmt.Sprintf("%s\t%s\t%s\t%s", HeaderID, HeaderReclaimable, HeaderSize, HeaderLastAccessed))
lines := strings.Split(string(out), "\n")

totalSize, err := parseTotalSize(lines[len(lines)-2])
if err != nil {
return nil, err
}

if len(lines) == 2 {
return &BuildctlPruneOutput{
TotalSize: totalSize,
Rows: nil,
}, nil
}

if err := tabReader.ParseHeader(lines[0]); err != nil {
return nil, err
}

var rows []BuildctlPruneOutputRow

for _, line := range lines[1 : len(lines)-2] {
// best effort parse row
id, _ := tabReader.ReadRow(line, HeaderID)
reclaimable, _ := tabReader.ReadRow(line, HeaderReclaimable)
size, _ := tabReader.ReadRow(line, HeaderSize)
lastAccessed, _ := tabReader.ReadRow(line, HeaderLastAccessed)
rows = append(rows, BuildctlPruneOutputRow{
ID: id,
Reclaimable: reclaimable,
Size: size,
LastAccessed: lastAccessed,
})
}

return &BuildctlPruneOutput{
TotalSize: totalSize,
Rows: rows,
}, nil
}

func parseTotalSize(line string) (string, error) {
if strings.HasPrefix(line, FinalizerTotal) {
return strings.TrimSpace(line[len(FinalizerTotal):]), nil
}
return "", fmt.Errorf("parse total size from buildctl prune command ouput, unexpected line, does not contains total size: %s", line)
}
148 changes: 148 additions & 0 deletions pkg/buildkitutil/buildctl_prune_output_parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
Copyright The containerd 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.
*/

/*
Portions from https://github.com/docker/cli/blob/v20.10.9/cli/command/image/build/context.go
Copyright (C) Docker authors.
Licensed under the Apache License, Version 2.0
NOTICE: https://github.com/docker/cli/blob/v20.10.9/NOTICE
*/

package buildkitutil

import (
"reflect"
"testing"
)

func TestParseBuildctlPruneOutput(t *testing.T) {
type args struct {
out []byte
}
tests := []struct {
name string
args args
want *BuildctlPruneOutput
wantErr bool
}{
{
name: "builtctl prune frees several spaces",
args: args{out: []byte(`ID RECLAIMABLE SIZE LAST ACCESSED
4kaw6aqapf3qvmqj3jskct89r true 0B
i0ys2vka15idnu952zc3le36o* true 0B
zn1cxnxqyxa18mzi5u2bnnqqk* true 4.10kB
x43clde10rasyy42vvzwo5w1r true 156B
kt007lnewdnssgukycqux8rcc true 0B
th54xgnz12r1rsgaoy55pfla1 true 0B
66b6kbtv4iul87bzibzxwgo51 true 0B
nlanvxjtqluiipuwva3uwfwvk true 0B
yh3oha6crdq9y34lmi2jshwhb true 0B
hai2h463u7o8xuybj0339e139 true 0B
Total: 4.25kB
`)},
want: &BuildctlPruneOutput{
TotalSize: "584.31MB",
Rows: []BuildctlPruneOutputRow{
{
ID: "4kaw6aqapf3qvmqj3jskct89r",
Reclaimable: "true",
Size: "0B",
LastAccessed: "",
},
{
ID: "i0ys2vka15idnu952zc3le36o*",
Reclaimable: "true",
Size: "0B",
LastAccessed: "",
},
{
ID: "zn1cxnxqyxa18mzi5u2bnnqqk*",
Reclaimable: "true",
Size: "4.10kB",
LastAccessed: "",
},
{
ID: "x43clde10rasyy42vvzwo5w1r",
Reclaimable: "true",
Size: "156B",
LastAccessed: "",
},
{
ID: "kt007lnewdnssgukycqux8rcc",
Reclaimable: "true",
Size: "0B",
LastAccessed: "",
},
{
ID: "th54xgnz12r1rsgaoy55pfla1",
Reclaimable: "true",
Size: "0B",
LastAccessed: "",
},
{
ID: "66b6kbtv4iul87bzibzxwgo51",
Reclaimable: "true",
Size: "0B",
LastAccessed: "",
},
{
ID: "nlanvxjtqluiipuwva3uwfwvk",
Reclaimable: "true",
Size: "0B",
LastAccessed: "",
},
{
ID: "yh3oha6crdq9y34lmi2jshwhb",
Reclaimable: "true",
Size: "0B",
LastAccessed: "",
},
{
ID: "hai2h463u7o8xuybj0339e139",
Reclaimable: "true",
Size: "0B",
LastAccessed: "",
},
},
},
wantErr: false,
},
{
name: "buildctl prune frees no spaces",
args: args{
out: []byte(`Total: 0B
`),
},
want: &BuildctlPruneOutput{
TotalSize: "0B",
Rows: nil,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseBuildctlPruneTableOutput(tt.args.out)
if (err != nil) != tt.wantErr {
t.Errorf("ParseBuildctlPruneTableOutput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseBuildctlPruneTableOutput() got = %v, want %v", got, tt.want)
}
})
}
}
12 changes: 11 additions & 1 deletion pkg/tabutil/tabutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,21 @@ func (r *TabReader) ReadRow(row, key string) (string, bool) {
if !ok {
return "", false
}
if idx.start > len(row) {
return "", false
}
Comment thread
STRRL marked this conversation as resolved.
var value string
if idx.end == -1 {
value = row[idx.start:]
} else {
value = row[idx.start:idx.end]
end := min(idx.end, len(row))
value = row[idx.start:end]
}
return strings.TrimSpace(value), true
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
34 changes: 34 additions & 0 deletions pkg/tabutil/tabutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,37 @@ func TestTabReader(t *testing.T) {
value, _ = reader.ReadRow(tabRows[2], "b")
assert.Equal(t, value, "456")
}

func TestTabReaderParseTrimmedRow(t *testing.T) {
// for example, row 2 and row 3 are trimmed because they do not contain value for the header c
tabRows := strings.Split(`a b c
1 2
123 456`, "\n")
reader := NewReader("a\tb\tc\t")

err := reader.ParseHeader(tabRows[0])
assert.NilError(t, err)

var (
value string
ok bool
)
value, ok = reader.ReadRow(tabRows[1], "a")
assert.Equal(t, value, "1")
assert.Equal(t, ok, true)
value, ok = reader.ReadRow(tabRows[1], "b")
assert.Equal(t, value, "2")
assert.Equal(t, ok, true)
value, ok = reader.ReadRow(tabRows[1], "c")
assert.Equal(t, value, "")
assert.Equal(t, ok, false)
value, ok = reader.ReadRow(tabRows[2], "a")
assert.Equal(t, value, "123")
assert.Equal(t, ok, true)
value, ok = reader.ReadRow(tabRows[2], "b")
assert.Equal(t, value, "456")
assert.Equal(t, ok, true)
value, ok = reader.ReadRow(tabRows[2], "c")
assert.Equal(t, value, "")
assert.Equal(t, ok, false)
}